From b91b1dbb4830b54758c15e39cb9f7d19f1e2fce0 Mon Sep 17 00:00:00 2001 From: Joshua Watt Date: Tue, 4 Jun 2024 14:44:31 -0600 Subject: [PATCH 01/14] jsonschema: Remove unused definitons These were left over from a previous implementation and are not needed --- src/shacl2code/lang/templates/jsonschema.j2 | 12 ------------ tests/expect/jsonschema/test-context.json | 12 ------------ tests/expect/jsonschema/test.json | 12 ------------ 3 files changed, 36 deletions(-) diff --git a/src/shacl2code/lang/templates/jsonschema.j2 b/src/shacl2code/lang/templates/jsonschema.j2 index 1e18d83e..68edbb77 100644 --- a/src/shacl2code/lang/templates/jsonschema.j2 +++ b/src/shacl2code/lang/templates/jsonschema.j2 @@ -248,18 +248,6 @@ "anyURI": { "type": "string" }, - "DateTime": { - "type": "string" - }, - "MediaType": { - "type": "string" - }, - "SemVer": { - "type": "string" - }, - "Extension": { - "type": "string" - }, "SHACLClass": { "type": "object", "properties": { diff --git a/tests/expect/jsonschema/test-context.json b/tests/expect/jsonschema/test-context.json index 4113dc0a..1bc3421c 100644 --- a/tests/expect/jsonschema/test-context.json +++ b/tests/expect/jsonschema/test-context.json @@ -1166,18 +1166,6 @@ "anyURI": { "type": "string" }, - "DateTime": { - "type": "string" - }, - "MediaType": { - "type": "string" - }, - "SemVer": { - "type": "string" - }, - "Extension": { - "type": "string" - }, "SHACLClass": { "type": "object", "properties": { diff --git a/tests/expect/jsonschema/test.json b/tests/expect/jsonschema/test.json index 8e30ae9b..4a8e913d 100644 --- a/tests/expect/jsonschema/test.json +++ b/tests/expect/jsonschema/test.json @@ -1162,18 +1162,6 @@ "anyURI": { "type": "string" }, - "DateTime": { - "type": "string" - }, - "MediaType": { - "type": "string" - }, - "SemVer": { - "type": "string" - }, - "Extension": { - "type": "string" - }, "SHACLClass": { "type": "object", "properties": { From a7ddae291c086fc869ce3a90bc4ab9e97c9affe9 Mon Sep 17 00:00:00 2001 From: Joshua Watt Date: Fri, 14 Jun 2024 09:04:29 -0600 Subject: [PATCH 02/14] python: Fix required abstract properties If a (concrete) object had a required property that was an abstract class, it would not be able to be initialized because the abstract class could not be created on initialization. To fix this, rework the way abstract detection is done, and make ObjectProp() initialize the property to None if the class is abstract --- src/shacl2code/lang/templates/python.j2 | 27 ++++--- tests/data/model/test.ttl | 15 ++++ tests/expect/jsonschema/test-context.json | 49 +++++++++++++ tests/expect/jsonschema/test.json | 49 +++++++++++++ tests/expect/python/test-context.py | 86 ++++++++++++++--------- tests/expect/python/test.py | 85 +++++++++++++--------- tests/expect/raw/test-context.txt | 2 + tests/expect/raw/test.txt | 1 + tests/test_python.py | 26 +++++++ 9 files changed, 260 insertions(+), 80 deletions(-) diff --git a/src/shacl2code/lang/templates/python.j2 b/src/shacl2code/lang/templates/python.j2 index 5afa21ce..04786f06 100644 --- a/src/shacl2code/lang/templates/python.j2 +++ b/src/shacl2code/lang/templates/python.j2 @@ -230,7 +230,7 @@ class ObjectProp(Property): self.required = required def init(self): - if self.required: + if self.required and not self.cls.IS_ABSTRACT: return self.cls() return None @@ -497,7 +497,7 @@ def is_blank_node(s): return True -def register(type_iri, compact_type=None): +def register(type_iri, *, compact_type=None, abstract=False): def add_class(key, c): assert ( key not in SHACLObject.CLASSES @@ -510,10 +510,12 @@ def register(type_iri, compact_type=None): ), f"{c.__name__} is not derived from SHACLObject" c._OBJ_TYPE = type_iri - add_class(type_iri, c) + c.IS_ABSTRACT = abstract + if not abstract: + add_class(type_iri, c) c._OBJ_COMPACT_TYPE = compact_type - if compact_type: + if compact_type and not abstract: add_class(compact_type, c) # Registration is deferred until the first instance of class is created @@ -532,8 +534,14 @@ class SHACLObject(object): CLASSES = {} NODE_KIND = NodeKind.BlankNodeOrIRI ID_ALIAS = None + IS_ABSTRACT = True def __init__(self, **kwargs): + if self.__class__.IS_ABSTRACT: + raise NotImplementedError( + f"{self.__class__.__name__} is abstract and cannot be implemented" + ) + with register_lock: cls = self.__class__ if cls._NEEDS_REG: @@ -1940,9 +1948,7 @@ CONTEXT_URLS = [ #{{ (" " + l).rstrip() }} {%- endfor %} {%- endif %} -{%- if not class.is_abstract %} -@register("{{ class._id }}"{%- if context.compact(class._id) != class._id %}, "{{ context.compact(class._id) }}"{%- endif %}) -{%- endif %} +@register("{{ class._id }}"{%- if context.compact(class._id) != class._id %}, compact_type="{{ context.compact(class._id) }}"{%- endif %}, abstract={{ class.is_abstract }}) class {{ varname(*class.clsname) }}( {%- if class.is_extensible -%} SHACLExtensibleObject{{", "}} @@ -1971,13 +1977,6 @@ class {{ varname(*class.clsname) }}( {%- endif %} {{ varname(member.varname) }} = "{{ member._id }}" {%- endfor %} - {%- if class.is_abstract %} - - def __init__(self, *args, **kwargs): - if self.__class__ is {{ varname(*class.clsname) }}: - raise NotImplementedError(f"{self.__class__.__name__} is abstract and cannot be implemented") - super().__init__(*args, **kwargs) - {%- endif %} {%- if class.properties %} @classmethod diff --git a/tests/data/model/test.ttl b/tests/data/model/test.ttl index 0930e67d..37a195ab 100644 --- a/tests/data/model/test.ttl +++ b/tests/data/model/test.ttl @@ -495,3 +495,18 @@ rdfs:subClassOf ; rdfs:comment "A concrete class" . + + a rdf:Class, sh:NodeShape, owl:Class ; + rdfs:comment "A class with a mandatory abstract class" ; + sh:property [ + sh:class ; + sh:path ; + sh:minCount 1 ; + sh:maxCount 1 + ] + . + + a rdf:Property ; + rdfs:comment "A required abstract class property" ; + rdfs:range + . diff --git a/tests/expect/jsonschema/test-context.json b/tests/expect/jsonschema/test-context.json index 1bc3421c..7f815a3c 100644 --- a/tests/expect/jsonschema/test-context.json +++ b/tests/expect/jsonschema/test-context.json @@ -577,6 +577,53 @@ } ] }, + "requiredabstract": { + "allOf": [ + { + "type": "object", + "properties": { + "@id": { "$ref": "#/$defs/BlankNodeOrIRI" }, + "@type": { + "oneOf": [ + { "const": "required-abstract" } + ] + } + } + }, + { "$ref": "#/$defs/requiredabstract_props" } + ] + }, + "requiredabstract_derived": { + "anyOf": [ + { + "type": "object", + "unevaluatedProperties": false, + "anyOf": [ + { "$ref": "#/$defs/requiredabstract" } + ] + }, + { "$ref": "#/$defs/BlankNodeOrIRI" } + ] + }, + "requiredabstract_props": { + "allOf": [ + { "$ref": "#/$defs/SHACLClass" }, + { + "type": "object", + "properties": { + "required-abstract/abstract-class-prop": { + "$ref": "#/$defs/prop_requiredabstract_requiredabstractabstractclassprop" + } + }, + "required": [ + "required-abstract/abstract-class-prop" + ] + } + ] + }, + "prop_requiredabstract_requiredabstractabstractclassprop": { + "$ref": "#/$defs/abstractclass_derived" + }, "testanotherclass": { "allOf": [ { @@ -1186,6 +1233,7 @@ "node-kind-iri-or-blank", "non-shape-class", "parent-class", + "required-abstract", "test-another-class", "test-class", "test-class-required", @@ -1214,6 +1262,7 @@ { "$ref": "#/$defs/nodekindiriorblank" }, { "$ref": "#/$defs/nonshapeclass" }, { "$ref": "#/$defs/parentclass" }, + { "$ref": "#/$defs/requiredabstract" }, { "$ref": "#/$defs/testanotherclass" }, { "$ref": "#/$defs/testclass" }, { "$ref": "#/$defs/testclassrequired" }, diff --git a/tests/expect/jsonschema/test.json b/tests/expect/jsonschema/test.json index 4a8e913d..c433b123 100644 --- a/tests/expect/jsonschema/test.json +++ b/tests/expect/jsonschema/test.json @@ -573,6 +573,53 @@ } ] }, + "http_exampleorgrequiredabstract": { + "allOf": [ + { + "type": "object", + "properties": { + "@id": { "$ref": "#/$defs/BlankNodeOrIRI" }, + "@type": { + "oneOf": [ + { "const": "http://example.org/required-abstract" } + ] + } + } + }, + { "$ref": "#/$defs/http_exampleorgrequiredabstract_props" } + ] + }, + "http_exampleorgrequiredabstract_derived": { + "anyOf": [ + { + "type": "object", + "unevaluatedProperties": false, + "anyOf": [ + { "$ref": "#/$defs/http_exampleorgrequiredabstract" } + ] + }, + { "$ref": "#/$defs/BlankNodeOrIRI" } + ] + }, + "http_exampleorgrequiredabstract_props": { + "allOf": [ + { "$ref": "#/$defs/SHACLClass" }, + { + "type": "object", + "properties": { + "http://example.org/required-abstract/abstract-class-prop": { + "$ref": "#/$defs/prop_http_exampleorgrequiredabstract_abstractclassprop" + } + }, + "required": [ + "http://example.org/required-abstract/abstract-class-prop" + ] + } + ] + }, + "prop_http_exampleorgrequiredabstract_abstractclassprop": { + "$ref": "#/$defs/http_exampleorgabstractclass_derived" + }, "http_exampleorgtestanotherclass": { "allOf": [ { @@ -1182,6 +1229,7 @@ "http://example.org/node-kind-iri-or-blank", "http://example.org/non-shape-class", "http://example.org/parent-class", + "http://example.org/required-abstract", "http://example.org/test-another-class", "http://example.org/test-class", "http://example.org/test-class-required", @@ -1210,6 +1258,7 @@ { "$ref": "#/$defs/http_exampleorgnodekindiriorblank" }, { "$ref": "#/$defs/http_exampleorgnonshapeclass" }, { "$ref": "#/$defs/http_exampleorgparentclass" }, + { "$ref": "#/$defs/http_exampleorgrequiredabstract" }, { "$ref": "#/$defs/http_exampleorgtestanotherclass" }, { "$ref": "#/$defs/http_exampleorgtestclass" }, { "$ref": "#/$defs/http_exampleorgtestclassrequired" }, diff --git a/tests/expect/python/test-context.py b/tests/expect/python/test-context.py index 1ad47b27..01681f9e 100755 --- a/tests/expect/python/test-context.py +++ b/tests/expect/python/test-context.py @@ -230,7 +230,7 @@ def __init__(self, cls, required): self.required = required def init(self): - if self.required: + if self.required and not self.cls.IS_ABSTRACT: return self.cls() return None @@ -497,7 +497,7 @@ def is_blank_node(s): return True -def register(type_iri, compact_type=None): +def register(type_iri, *, compact_type=None, abstract=False): def add_class(key, c): assert ( key not in SHACLObject.CLASSES @@ -510,10 +510,12 @@ def decorator(c): ), f"{c.__name__} is not derived from SHACLObject" c._OBJ_TYPE = type_iri - add_class(type_iri, c) + c.IS_ABSTRACT = abstract + if not abstract: + add_class(type_iri, c) c._OBJ_COMPACT_TYPE = compact_type - if compact_type: + if compact_type and not abstract: add_class(compact_type, c) # Registration is deferred until the first instance of class is created @@ -532,8 +534,14 @@ class SHACLObject(object): CLASSES = {} NODE_KIND = NodeKind.BlankNodeOrIRI ID_ALIAS = None + IS_ABSTRACT = True def __init__(self, **kwargs): + if self.__class__.IS_ABSTRACT: + raise NotImplementedError( + f"{self.__class__.__name__} is abstract and cannot be implemented" + ) + with register_lock: cls = self.__class__ if cls._NEEDS_REG: @@ -1921,31 +1929,23 @@ def callback(value, path): # CLASSES # An Abstract class +@register("http://example.org/abstract-class", compact_type="abstract-class", abstract=True) class abstract_class(SHACLObject): NODE_KIND = NodeKind.BlankNodeOrIRI NAMED_INDIVIDUALS = { } - def __init__(self, *args, **kwargs): - if self.__class__ is abstract_class: - raise NotImplementedError(f"{self.__class__.__name__} is abstract and cannot be implemented") - super().__init__(*args, **kwargs) - # An Abstract class using the SPDX type +@register("http://example.org/abstract-spdx-class", compact_type="abstract-spdx-class", abstract=True) class abstract_spdx_class(SHACLObject): NODE_KIND = NodeKind.BlankNodeOrIRI NAMED_INDIVIDUALS = { } - def __init__(self, *args, **kwargs): - if self.__class__ is abstract_spdx_class: - raise NotImplementedError(f"{self.__class__.__name__} is abstract and cannot be implemented") - super().__init__(*args, **kwargs) - # A concrete class -@register("http://example.org/concrete-class", "concrete-class") +@register("http://example.org/concrete-class", compact_type="concrete-class", abstract=False) class concrete_class(abstract_class): NODE_KIND = NodeKind.BlankNodeOrIRI NAMED_INDIVIDUALS = { @@ -1953,7 +1953,7 @@ class concrete_class(abstract_class): # A concrete class -@register("http://example.org/concrete-spdx-class", "concrete-spdx-class") +@register("http://example.org/concrete-spdx-class", compact_type="concrete-spdx-class", abstract=False) class concrete_spdx_class(abstract_spdx_class): NODE_KIND = NodeKind.BlankNodeOrIRI NAMED_INDIVIDUALS = { @@ -1961,7 +1961,7 @@ class concrete_spdx_class(abstract_spdx_class): # An enumerated type -@register("http://example.org/enumType", "enumType") +@register("http://example.org/enumType", compact_type="enumType", abstract=False) class enumType(SHACLObject): NODE_KIND = NodeKind.BlankNodeOrIRI NAMED_INDIVIDUALS = { @@ -1978,7 +1978,7 @@ class enumType(SHACLObject): # A class with an ID alias -@register("http://example.org/id-prop-class", "id-prop-class") +@register("http://example.org/id-prop-class", compact_type="id-prop-class", abstract=False) class id_prop_class(SHACLObject): NODE_KIND = NodeKind.BlankNodeOrIRI ID_ALIAS = "testid" @@ -1987,7 +1987,7 @@ class id_prop_class(SHACLObject): # A class that inherits its idPropertyName from the parent -@register("http://example.org/inherited-id-prop-class", "inherited-id-prop-class") +@register("http://example.org/inherited-id-prop-class", compact_type="inherited-id-prop-class", abstract=False) class inherited_id_prop_class(id_prop_class): NODE_KIND = NodeKind.BlankNodeOrIRI ID_ALIAS = "testid" @@ -1996,7 +1996,7 @@ class inherited_id_prop_class(id_prop_class): # A class to test links -@register("http://example.org/link-class", "link-class") +@register("http://example.org/link-class", compact_type="link-class", abstract=False) class link_class(SHACLObject): NODE_KIND = NodeKind.BlankNodeOrIRI NAMED_INDIVIDUALS = { @@ -2036,7 +2036,7 @@ def _register_props(cls): # A class derived from link-class -@register("http://example.org/link-derived-class", "link-derived-class") +@register("http://example.org/link-derived-class", compact_type="link-derived-class", abstract=False) class link_derived_class(link_class): NODE_KIND = NodeKind.BlankNodeOrIRI NAMED_INDIVIDUALS = { @@ -2044,7 +2044,7 @@ class link_derived_class(link_class): # A class that must be a blank node -@register("http://example.org/node-kind-blank", "node-kind-blank") +@register("http://example.org/node-kind-blank", compact_type="node-kind-blank", abstract=False) class node_kind_blank(link_class): NODE_KIND = NodeKind.BlankNode NAMED_INDIVIDUALS = { @@ -2052,7 +2052,7 @@ class node_kind_blank(link_class): # A class that must be an IRI -@register("http://example.org/node-kind-iri", "node-kind-iri") +@register("http://example.org/node-kind-iri", compact_type="node-kind-iri", abstract=False) class node_kind_iri(link_class): NODE_KIND = NodeKind.IRI NAMED_INDIVIDUALS = { @@ -2060,7 +2060,7 @@ class node_kind_iri(link_class): # A class that can be either a blank node or an IRI -@register("http://example.org/node-kind-iri-or-blank", "node-kind-iri-or-blank") +@register("http://example.org/node-kind-iri-or-blank", compact_type="node-kind-iri-or-blank", abstract=False) class node_kind_iri_or_blank(link_class): NODE_KIND = NodeKind.BlankNodeOrIRI NAMED_INDIVIDUALS = { @@ -2068,7 +2068,7 @@ class node_kind_iri_or_blank(link_class): # A class that is not a nodeshape -@register("http://example.org/non-shape-class", "non-shape-class") +@register("http://example.org/non-shape-class", compact_type="non-shape-class", abstract=False) class non_shape_class(SHACLObject): NODE_KIND = NodeKind.BlankNodeOrIRI NAMED_INDIVIDUALS = { @@ -2076,15 +2076,35 @@ class non_shape_class(SHACLObject): # The parent class -@register("http://example.org/parent-class", "parent-class") +@register("http://example.org/parent-class", compact_type="parent-class", abstract=False) class parent_class(SHACLObject): NODE_KIND = NodeKind.BlankNodeOrIRI NAMED_INDIVIDUALS = { } +# A class with a mandatory abstract class +@register("http://example.org/required-abstract", compact_type="required-abstract", abstract=False) +class required_abstract(SHACLObject): + NODE_KIND = NodeKind.BlankNodeOrIRI + NAMED_INDIVIDUALS = { + } + + @classmethod + def _register_props(cls): + super()._register_props() + # A required abstract class property + cls._add_property( + "required_abstract_abstract_class_prop", + ObjectProp(abstract_class, True), + iri="http://example.org/required-abstract/abstract-class-prop", + min_count=1, + compact="required-abstract/abstract-class-prop", + ) + + # Another class -@register("http://example.org/test-another-class", "test-another-class") +@register("http://example.org/test-another-class", compact_type="test-another-class", abstract=False) class test_another_class(SHACLObject): NODE_KIND = NodeKind.BlankNodeOrIRI NAMED_INDIVIDUALS = { @@ -2092,7 +2112,7 @@ class test_another_class(SHACLObject): # The test class -@register("http://example.org/test-class", "test-class") +@register("http://example.org/test-class", compact_type="test-class", abstract=False) class test_class(parent_class): NODE_KIND = NodeKind.BlankNodeOrIRI NAMED_INDIVIDUALS = { @@ -2300,7 +2320,7 @@ def _register_props(cls): ) -@register("http://example.org/test-class-required", "test-class-required") +@register("http://example.org/test-class-required", compact_type="test-class-required", abstract=False) class test_class_required(test_class): NODE_KIND = NodeKind.BlankNodeOrIRI NAMED_INDIVIDUALS = { @@ -2329,7 +2349,7 @@ def _register_props(cls): # A class derived from test-class -@register("http://example.org/test-derived-class", "test-derived-class") +@register("http://example.org/test-derived-class", compact_type="test-derived-class", abstract=False) class test_derived_class(test_class): NODE_KIND = NodeKind.BlankNodeOrIRI NAMED_INDIVIDUALS = { @@ -2348,7 +2368,7 @@ def _register_props(cls): # Derived class that sorts before the parent to test ordering -@register("http://example.org/aaa-derived-class", "aaa-derived-class") +@register("http://example.org/aaa-derived-class", compact_type="aaa-derived-class", abstract=False) class aaa_derived_class(parent_class): NODE_KIND = NodeKind.BlankNodeOrIRI NAMED_INDIVIDUALS = { @@ -2356,7 +2376,7 @@ class aaa_derived_class(parent_class): # A class that derives its nodeKind from parent -@register("http://example.org/derived-node-kind-iri", "derived-node-kind-iri") +@register("http://example.org/derived-node-kind-iri", compact_type="derived-node-kind-iri", abstract=False) class derived_node_kind_iri(node_kind_iri): NODE_KIND = NodeKind.IRI NAMED_INDIVIDUALS = { @@ -2364,7 +2384,7 @@ class derived_node_kind_iri(node_kind_iri): # An extensible class -@register("http://example.org/extensible-class", "extensible-class") +@register("http://example.org/extensible-class", compact_type="extensible-class", abstract=False) class extensible_class(SHACLExtensibleObject, link_class): NODE_KIND = NodeKind.BlankNodeOrIRI NAMED_INDIVIDUALS = { diff --git a/tests/expect/python/test.py b/tests/expect/python/test.py index 3e53c516..4d451ef0 100644 --- a/tests/expect/python/test.py +++ b/tests/expect/python/test.py @@ -230,7 +230,7 @@ def __init__(self, cls, required): self.required = required def init(self): - if self.required: + if self.required and not self.cls.IS_ABSTRACT: return self.cls() return None @@ -497,7 +497,7 @@ def is_blank_node(s): return True -def register(type_iri, compact_type=None): +def register(type_iri, *, compact_type=None, abstract=False): def add_class(key, c): assert ( key not in SHACLObject.CLASSES @@ -510,10 +510,12 @@ def decorator(c): ), f"{c.__name__} is not derived from SHACLObject" c._OBJ_TYPE = type_iri - add_class(type_iri, c) + c.IS_ABSTRACT = abstract + if not abstract: + add_class(type_iri, c) c._OBJ_COMPACT_TYPE = compact_type - if compact_type: + if compact_type and not abstract: add_class(compact_type, c) # Registration is deferred until the first instance of class is created @@ -532,8 +534,14 @@ class SHACLObject(object): CLASSES = {} NODE_KIND = NodeKind.BlankNodeOrIRI ID_ALIAS = None + IS_ABSTRACT = True def __init__(self, **kwargs): + if self.__class__.IS_ABSTRACT: + raise NotImplementedError( + f"{self.__class__.__name__} is abstract and cannot be implemented" + ) + with register_lock: cls = self.__class__ if cls._NEEDS_REG: @@ -1920,31 +1928,23 @@ def callback(value, path): # CLASSES # An Abstract class +@register("http://example.org/abstract-class", abstract=True) class http_example_org_abstract_class(SHACLObject): NODE_KIND = NodeKind.BlankNodeOrIRI NAMED_INDIVIDUALS = { } - def __init__(self, *args, **kwargs): - if self.__class__ is http_example_org_abstract_class: - raise NotImplementedError(f"{self.__class__.__name__} is abstract and cannot be implemented") - super().__init__(*args, **kwargs) - # An Abstract class using the SPDX type +@register("http://example.org/abstract-spdx-class", abstract=True) class http_example_org_abstract_spdx_class(SHACLObject): NODE_KIND = NodeKind.BlankNodeOrIRI NAMED_INDIVIDUALS = { } - def __init__(self, *args, **kwargs): - if self.__class__ is http_example_org_abstract_spdx_class: - raise NotImplementedError(f"{self.__class__.__name__} is abstract and cannot be implemented") - super().__init__(*args, **kwargs) - # A concrete class -@register("http://example.org/concrete-class") +@register("http://example.org/concrete-class", abstract=False) class http_example_org_concrete_class(http_example_org_abstract_class): NODE_KIND = NodeKind.BlankNodeOrIRI NAMED_INDIVIDUALS = { @@ -1952,7 +1952,7 @@ class http_example_org_concrete_class(http_example_org_abstract_class): # A concrete class -@register("http://example.org/concrete-spdx-class") +@register("http://example.org/concrete-spdx-class", abstract=False) class http_example_org_concrete_spdx_class(http_example_org_abstract_spdx_class): NODE_KIND = NodeKind.BlankNodeOrIRI NAMED_INDIVIDUALS = { @@ -1960,7 +1960,7 @@ class http_example_org_concrete_spdx_class(http_example_org_abstract_spdx_class) # An enumerated type -@register("http://example.org/enumType") +@register("http://example.org/enumType", abstract=False) class http_example_org_enumType(SHACLObject): NODE_KIND = NodeKind.BlankNodeOrIRI NAMED_INDIVIDUALS = { @@ -1977,7 +1977,7 @@ class http_example_org_enumType(SHACLObject): # A class with an ID alias -@register("http://example.org/id-prop-class") +@register("http://example.org/id-prop-class", abstract=False) class http_example_org_id_prop_class(SHACLObject): NODE_KIND = NodeKind.BlankNodeOrIRI ID_ALIAS = "testid" @@ -1986,7 +1986,7 @@ class http_example_org_id_prop_class(SHACLObject): # A class that inherits its idPropertyName from the parent -@register("http://example.org/inherited-id-prop-class") +@register("http://example.org/inherited-id-prop-class", abstract=False) class http_example_org_inherited_id_prop_class(http_example_org_id_prop_class): NODE_KIND = NodeKind.BlankNodeOrIRI ID_ALIAS = "testid" @@ -1995,7 +1995,7 @@ class http_example_org_inherited_id_prop_class(http_example_org_id_prop_class): # A class to test links -@register("http://example.org/link-class") +@register("http://example.org/link-class", abstract=False) class http_example_org_link_class(SHACLObject): NODE_KIND = NodeKind.BlankNodeOrIRI NAMED_INDIVIDUALS = { @@ -2031,7 +2031,7 @@ def _register_props(cls): # A class derived from link-class -@register("http://example.org/link-derived-class") +@register("http://example.org/link-derived-class", abstract=False) class http_example_org_link_derived_class(http_example_org_link_class): NODE_KIND = NodeKind.BlankNodeOrIRI NAMED_INDIVIDUALS = { @@ -2039,7 +2039,7 @@ class http_example_org_link_derived_class(http_example_org_link_class): # A class that must be a blank node -@register("http://example.org/node-kind-blank") +@register("http://example.org/node-kind-blank", abstract=False) class http_example_org_node_kind_blank(http_example_org_link_class): NODE_KIND = NodeKind.BlankNode NAMED_INDIVIDUALS = { @@ -2047,7 +2047,7 @@ class http_example_org_node_kind_blank(http_example_org_link_class): # A class that must be an IRI -@register("http://example.org/node-kind-iri") +@register("http://example.org/node-kind-iri", abstract=False) class http_example_org_node_kind_iri(http_example_org_link_class): NODE_KIND = NodeKind.IRI NAMED_INDIVIDUALS = { @@ -2055,7 +2055,7 @@ class http_example_org_node_kind_iri(http_example_org_link_class): # A class that can be either a blank node or an IRI -@register("http://example.org/node-kind-iri-or-blank") +@register("http://example.org/node-kind-iri-or-blank", abstract=False) class http_example_org_node_kind_iri_or_blank(http_example_org_link_class): NODE_KIND = NodeKind.BlankNodeOrIRI NAMED_INDIVIDUALS = { @@ -2063,7 +2063,7 @@ class http_example_org_node_kind_iri_or_blank(http_example_org_link_class): # A class that is not a nodeshape -@register("http://example.org/non-shape-class") +@register("http://example.org/non-shape-class", abstract=False) class http_example_org_non_shape_class(SHACLObject): NODE_KIND = NodeKind.BlankNodeOrIRI NAMED_INDIVIDUALS = { @@ -2071,15 +2071,34 @@ class http_example_org_non_shape_class(SHACLObject): # The parent class -@register("http://example.org/parent-class") +@register("http://example.org/parent-class", abstract=False) class http_example_org_parent_class(SHACLObject): NODE_KIND = NodeKind.BlankNodeOrIRI NAMED_INDIVIDUALS = { } +# A class with a mandatory abstract class +@register("http://example.org/required-abstract", abstract=False) +class http_example_org_required_abstract(SHACLObject): + NODE_KIND = NodeKind.BlankNodeOrIRI + NAMED_INDIVIDUALS = { + } + + @classmethod + def _register_props(cls): + super()._register_props() + # A required abstract class property + cls._add_property( + "abstract_class_prop", + ObjectProp(http_example_org_abstract_class, True), + iri="http://example.org/required-abstract/abstract-class-prop", + min_count=1, + ) + + # Another class -@register("http://example.org/test-another-class") +@register("http://example.org/test-another-class", abstract=False) class http_example_org_test_another_class(SHACLObject): NODE_KIND = NodeKind.BlankNodeOrIRI NAMED_INDIVIDUALS = { @@ -2087,7 +2106,7 @@ class http_example_org_test_another_class(SHACLObject): # The test class -@register("http://example.org/test-class") +@register("http://example.org/test-class", abstract=False) class http_example_org_test_class(http_example_org_parent_class): NODE_KIND = NodeKind.BlankNodeOrIRI NAMED_INDIVIDUALS = { @@ -2269,7 +2288,7 @@ def _register_props(cls): ) -@register("http://example.org/test-class-required") +@register("http://example.org/test-class-required", abstract=False) class http_example_org_test_class_required(http_example_org_test_class): NODE_KIND = NodeKind.BlankNodeOrIRI NAMED_INDIVIDUALS = { @@ -2296,7 +2315,7 @@ def _register_props(cls): # A class derived from test-class -@register("http://example.org/test-derived-class") +@register("http://example.org/test-derived-class", abstract=False) class http_example_org_test_derived_class(http_example_org_test_class): NODE_KIND = NodeKind.BlankNodeOrIRI NAMED_INDIVIDUALS = { @@ -2314,7 +2333,7 @@ def _register_props(cls): # Derived class that sorts before the parent to test ordering -@register("http://example.org/aaa-derived-class") +@register("http://example.org/aaa-derived-class", abstract=False) class http_example_org_aaa_derived_class(http_example_org_parent_class): NODE_KIND = NodeKind.BlankNodeOrIRI NAMED_INDIVIDUALS = { @@ -2322,7 +2341,7 @@ class http_example_org_aaa_derived_class(http_example_org_parent_class): # A class that derives its nodeKind from parent -@register("http://example.org/derived-node-kind-iri") +@register("http://example.org/derived-node-kind-iri", abstract=False) class http_example_org_derived_node_kind_iri(http_example_org_node_kind_iri): NODE_KIND = NodeKind.IRI NAMED_INDIVIDUALS = { @@ -2330,7 +2349,7 @@ class http_example_org_derived_node_kind_iri(http_example_org_node_kind_iri): # An extensible class -@register("http://example.org/extensible-class") +@register("http://example.org/extensible-class", abstract=False) class http_example_org_extensible_class(SHACLExtensibleObject, http_example_org_link_class): NODE_KIND = NodeKind.BlankNodeOrIRI NAMED_INDIVIDUALS = { diff --git a/tests/expect/raw/test-context.txt b/tests/expect/raw/test-context.txt index e9e7a54e..156fc912 100644 --- a/tests/expect/raw/test-context.txt +++ b/tests/expect/raw/test-context.txt @@ -29,6 +29,8 @@ http://example.org/non-shape-class: non-shape-class http://example.org/parent-class: parent-class +http://example.org/required-abstract: required-abstract + http://example.org/test-another-class: test-another-class http://example.org/test-class: test-class diff --git a/tests/expect/raw/test.txt b/tests/expect/raw/test.txt index c454f186..d9f9c471 100644 --- a/tests/expect/raw/test.txt +++ b/tests/expect/raw/test.txt @@ -14,6 +14,7 @@ Class(_id='http://example.org/node-kind-iri', clsname=['http', '//example.org/no Class(_id='http://example.org/node-kind-iri-or-blank', clsname=['http', '//example.org/node-kind-iri-or-blank'], parent_ids=['http://example.org/link-class'], derived_ids=[], properties=[], comment='A class that can be either a blank node or an IRI', id_property=None, node_kind=rdflib.term.URIRef('http://www.w3.org/ns/shacl#BlankNodeOrIRI'), is_extensible=False, is_abstract=False, named_individuals=[]) Class(_id='http://example.org/non-shape-class', clsname=['http', '//example.org/non-shape-class'], parent_ids=[], derived_ids=[], properties=[], comment='A class that is not a nodeshape', id_property=None, node_kind=rdflib.term.URIRef('http://www.w3.org/ns/shacl#BlankNodeOrIRI'), is_extensible=False, is_abstract=False, named_individuals=[]) Class(_id='http://example.org/parent-class', clsname=['http', '//example.org/parent-class'], parent_ids=[], derived_ids=['http://example.org/aaa-derived-class', 'http://example.org/test-class'], properties=[], comment='The parent class', id_property=None, node_kind=rdflib.term.URIRef('http://www.w3.org/ns/shacl#BlankNodeOrIRI'), is_extensible=False, is_abstract=False, named_individuals=[]) +Class(_id='http://example.org/required-abstract', clsname=['http', '//example.org/required-abstract'], parent_ids=[], derived_ids=[], properties=[Property(path='http://example.org/required-abstract/abstract-class-prop', varname='abstract-class-prop', comment='A required abstract class property', max_count=1, min_count=1, enum_values=None, class_id='http://example.org/abstract-class', datatype='', pattern='')], comment='A class with a mandatory abstract class', id_property=None, node_kind=rdflib.term.URIRef('http://www.w3.org/ns/shacl#BlankNodeOrIRI'), is_extensible=False, is_abstract=False, named_individuals=[]) Class(_id='http://example.org/test-another-class', clsname=['http', '//example.org/test-another-class'], parent_ids=[], derived_ids=[], properties=[], comment='Another class', id_property=None, node_kind=rdflib.term.URIRef('http://www.w3.org/ns/shacl#BlankNodeOrIRI'), is_extensible=False, is_abstract=False, named_individuals=[]) Class(_id='http://example.org/test-class', clsname=['http', '//example.org/test-class'], parent_ids=['http://example.org/parent-class'], derived_ids=['http://example.org/test-class-required', 'http://example.org/test-derived-class'], properties=[Property(path='http://example.org/encode', varname='encode', comment='A property that conflicts with an existing SHACLObject property', max_count=1, min_count=None, enum_values=None, class_id='', datatype='http://www.w3.org/2001/XMLSchema#string', pattern=''), Property(path='http://example.org/import', varname='import', comment='A property that is a keyword', max_count=1, min_count=None, enum_values=None, class_id='', datatype='http://www.w3.org/2001/XMLSchema#string', pattern=''), Property(path='http://example.org/test-class/anyuri-prop', varname='anyuri-prop', comment='a URI', max_count=1, min_count=None, enum_values=None, class_id='', datatype='http://www.w3.org/2001/XMLSchema#anyURI', pattern=''), Property(path='http://example.org/test-class/boolean-prop', varname='boolean-prop', comment='a boolean property', max_count=1, min_count=None, enum_values=None, class_id='', datatype='http://www.w3.org/2001/XMLSchema#boolean', pattern=''), Property(path='http://example.org/test-class/class-list-prop', varname='class-list-prop', comment='A test-class list property', max_count=None, min_count=None, enum_values=None, class_id='http://example.org/test-class', datatype='', pattern=''), Property(path='http://example.org/test-class/class-prop', varname='class-prop', comment='A test-class property', max_count=1, min_count=None, enum_values=None, class_id='http://example.org/test-class', datatype='', pattern=''), Property(path='http://example.org/test-class/class-prop-no-class', varname='class-prop-no-class', comment='A test-class property with no sh:class', max_count=1, min_count=None, enum_values=None, class_id='http://example.org/test-class', datatype='', pattern=''), Property(path='http://example.org/test-class/datetime-list-prop', varname='datetime-list-prop', comment='A datetime list property', max_count=None, min_count=None, enum_values=None, class_id='', datatype='http://www.w3.org/2001/XMLSchema#dateTime', pattern=''), Property(path='http://example.org/test-class/datetime-scalar-prop', varname='datetime-scalar-prop', comment='A scalar datetime property', max_count=1, min_count=None, enum_values=None, class_id='', datatype='http://www.w3.org/2001/XMLSchema#dateTime', pattern=''), Property(path='http://example.org/test-class/datetimestamp-scalar-prop', varname='datetimestamp-scalar-prop', comment='A scalar dateTimeStamp property', max_count=1, min_count=None, enum_values=None, class_id='', datatype='http://www.w3.org/2001/XMLSchema#dateTimeStamp', pattern=''), Property(path='http://example.org/test-class/enum-list-prop', varname='enum-list-prop', comment='A enum list property', max_count=None, min_count=None, enum_values=[rdflib.term.URIRef('http://example.org/enumType/bar'), rdflib.term.URIRef('http://example.org/enumType/foo'), rdflib.term.URIRef('http://example.org/enumType/nolabel'), rdflib.term.URIRef('http://example.org/enumType/non-named-individual')], class_id='http://example.org/enumType', datatype='', pattern=''), Property(path='http://example.org/test-class/enum-prop', varname='enum-prop', comment='A enum property', max_count=1, min_count=None, enum_values=[rdflib.term.URIRef('http://example.org/enumType/bar'), rdflib.term.URIRef('http://example.org/enumType/foo'), rdflib.term.URIRef('http://example.org/enumType/nolabel'), rdflib.term.URIRef('http://example.org/enumType/non-named-individual')], class_id='http://example.org/enumType', datatype='', pattern=''), Property(path='http://example.org/test-class/enum-prop-no-class', varname='enum-prop-no-class', comment='A enum property with no sh:class', max_count=1, min_count=None, enum_values=[rdflib.term.URIRef('http://example.org/enumType/bar'), rdflib.term.URIRef('http://example.org/enumType/foo'), rdflib.term.URIRef('http://example.org/enumType/nolabel'), rdflib.term.URIRef('http://example.org/enumType/non-named-individual')], class_id='http://example.org/enumType', datatype='', pattern=''), Property(path='http://example.org/test-class/float-prop', varname='float-prop', comment='a float property', max_count=1, min_count=None, enum_values=None, class_id='', datatype='http://www.w3.org/2001/XMLSchema#decimal', pattern=''), Property(path='http://example.org/test-class/integer-prop', varname='integer-prop', comment='a non-negative integer', max_count=1, min_count=None, enum_values=None, class_id='', datatype='http://www.w3.org/2001/XMLSchema#integer', pattern=''), Property(path='http://example.org/test-class/named-property', varname=rdflib.term.Literal('named_property'), comment='A named property', max_count=1, min_count=None, enum_values=None, class_id='', datatype='http://www.w3.org/2001/XMLSchema#string', pattern=''), Property(path='http://example.org/test-class/non-shape', varname='non-shape', comment='A class with no shape', max_count=1, min_count=None, enum_values=None, class_id='http://example.org/non-shape-class', datatype='', pattern=''), Property(path='http://example.org/test-class/nonnegative-integer-prop', varname='nonnegative-integer-prop', comment='a non-negative integer', max_count=1, min_count=None, enum_values=None, class_id='', datatype='http://www.w3.org/2001/XMLSchema#nonNegativeInteger', pattern=''), Property(path='http://example.org/test-class/positive-integer-prop', varname='positive-integer-prop', comment='A positive integer', max_count=1, min_count=None, enum_values=None, class_id='', datatype='http://www.w3.org/2001/XMLSchema#positiveInteger', pattern=''), Property(path='http://example.org/test-class/regex', varname='regex', comment='A regex validated string', max_count=1, min_count=None, enum_values=None, class_id='', datatype='http://www.w3.org/2001/XMLSchema#string', pattern='^foo\\d'), Property(path='http://example.org/test-class/regex-datetime', varname='regex-datetime', comment='A regex dateTime', max_count=1, min_count=None, enum_values=None, class_id='', datatype='http://www.w3.org/2001/XMLSchema#dateTime', pattern='^\\d\\d\\d\\d-\\d\\d-\\d\\dT\\d\\d:\\d\\d:\\d\\d\\+01:00$'), Property(path='http://example.org/test-class/regex-datetimestamp', varname='regex-datetimestamp', comment='A regex dateTimeStamp', max_count=1, min_count=None, enum_values=None, class_id='', datatype='http://www.w3.org/2001/XMLSchema#dateTimeStamp', pattern='^\\d\\d\\d\\d-\\d\\d-\\d\\dT\\d\\d:\\d\\d:\\d\\dZ$'), Property(path='http://example.org/test-class/regex-list', varname='regex-list', comment='A regex validated string list', max_count=None, min_count=None, enum_values=None, class_id='', datatype='http://www.w3.org/2001/XMLSchema#string', pattern='^foo\\d'), Property(path='http://example.org/test-class/string-list-no-datatype', varname='string-list-no-datatype', comment='A string list property with no sh:datatype', max_count=None, min_count=None, enum_values=None, class_id='', datatype='http://www.w3.org/2001/XMLSchema#string', pattern=''), Property(path='http://example.org/test-class/string-list-prop', varname='string-list-prop', comment='A string list property', max_count=None, min_count=None, enum_values=None, class_id='', datatype='http://www.w3.org/2001/XMLSchema#string', pattern=''), Property(path='http://example.org/test-class/string-scalar-prop', varname='string-scalar-prop', comment='A scalar string propery', max_count=1, min_count=None, enum_values=None, class_id='', datatype='http://www.w3.org/2001/XMLSchema#string', pattern='')], comment='The test class', id_property=None, node_kind=rdflib.term.URIRef('http://www.w3.org/ns/shacl#BlankNodeOrIRI'), is_extensible=False, is_abstract=False, named_individuals=[]) Class(_id='http://example.org/test-class-required', clsname=['http', '//example.org/test-class-required'], parent_ids=['http://example.org/test-class'], derived_ids=[], properties=[Property(path='http://example.org/test-class/required-string-list-prop', varname='required-string-list-prop', comment='A required string list property', max_count=2, min_count=1, enum_values=None, class_id='', datatype='http://www.w3.org/2001/XMLSchema#string', pattern=''), Property(path='http://example.org/test-class/required-string-scalar-prop', varname='required-string-scalar-prop', comment='A required scalar string property', max_count=1, min_count=1, enum_values=None, class_id='', datatype='http://www.w3.org/2001/XMLSchema#string', pattern='')], comment='', id_property=None, node_kind=rdflib.term.URIRef('http://www.w3.org/ns/shacl#BlankNodeOrIRI'), is_extensible=False, is_abstract=False, named_individuals=[]) diff --git a/tests/test_python.py b/tests/test_python.py index eb942438..80791fab 100644 --- a/tests/test_python.py +++ b/tests/test_python.py @@ -1703,3 +1703,29 @@ def test_abstract_classes(model, abstract, concrete): # implemented cls = getattr(model, concrete) cls() + + +def test_required_abstract_class_property(model, tmp_path): + # Test that a class with a required property that references an abstract + # class can be instantiated + c = model.required_abstract() + assert c.required_abstract_abstract_class_prop is None + + outfile = tmp_path / "out.json" + objset = model.SHACLObjectSet() + objset.add(c) + s = model.JSONLDSerializer() + + # Atempting to serialize without assigning the property should fail + with outfile.open("wb") as f: + with pytest.raises(ValueError): + s.write(objset, f, indent=4) + + # Assigning a concrete class should succeed and allow serialization + c.required_abstract_abstract_class_prop = model.concrete_class() + with outfile.open("wb") as f: + s.write(objset, f, indent=4) + + # Deleting an abstract class property should return it to None + del c.required_abstract_abstract_class_prop + assert c.required_abstract_abstract_class_prop is None From fbab868dbbb42372da09d20c0f1dc6986743da26 Mon Sep 17 00:00:00 2001 From: Joshua Watt Date: Fri, 14 Jun 2024 11:22:34 -0600 Subject: [PATCH 03/14] python: Fix not being able to iterate by abstract type Fixes a bug where ObjectSet.foreach_type() would not iterate over objects that were derived from an abstract type. --- src/shacl2code/lang/templates/python.j2 | 5 ++--- tests/data/python/roundtrip.json | 4 ++++ tests/expect/python/test-context.py | 5 ++--- tests/expect/python/test.py | 5 ++--- tests/test_python.py | 10 ++++++++++ 5 files changed, 20 insertions(+), 9 deletions(-) diff --git a/src/shacl2code/lang/templates/python.j2 b/src/shacl2code/lang/templates/python.j2 index 04786f06..13dc2234 100644 --- a/src/shacl2code/lang/templates/python.j2 +++ b/src/shacl2code/lang/templates/python.j2 @@ -511,11 +511,10 @@ def register(type_iri, *, compact_type=None, abstract=False): c._OBJ_TYPE = type_iri c.IS_ABSTRACT = abstract - if not abstract: - add_class(type_iri, c) + add_class(type_iri, c) c._OBJ_COMPACT_TYPE = compact_type - if compact_type and not abstract: + if compact_type: add_class(compact_type, c) # Registration is deferred until the first instance of class is created diff --git a/tests/data/python/roundtrip.json b/tests/data/python/roundtrip.json index 2d19121b..4888908c 100644 --- a/tests/data/python/roundtrip.json +++ b/tests/data/python/roundtrip.json @@ -10,6 +10,10 @@ "http://serialize.example.com/link-derived-target" ] }, + { + "@type": "concrete-class", + "@id": "http://serialize.example.com/concrete-class" + }, { "@id": "http://serialize.example.com/link-derived-target", "@type": "link-derived-class" diff --git a/tests/expect/python/test-context.py b/tests/expect/python/test-context.py index 01681f9e..fc663985 100755 --- a/tests/expect/python/test-context.py +++ b/tests/expect/python/test-context.py @@ -511,11 +511,10 @@ def decorator(c): c._OBJ_TYPE = type_iri c.IS_ABSTRACT = abstract - if not abstract: - add_class(type_iri, c) + add_class(type_iri, c) c._OBJ_COMPACT_TYPE = compact_type - if compact_type and not abstract: + if compact_type: add_class(compact_type, c) # Registration is deferred until the first instance of class is created diff --git a/tests/expect/python/test.py b/tests/expect/python/test.py index 4d451ef0..771e2d94 100644 --- a/tests/expect/python/test.py +++ b/tests/expect/python/test.py @@ -511,11 +511,10 @@ def decorator(c): c._OBJ_TYPE = type_iri c.IS_ABSTRACT = abstract - if not abstract: - add_class(type_iri, c) + add_class(type_iri, c) c._OBJ_COMPACT_TYPE = compact_type - if compact_type and not abstract: + if compact_type: add_class(compact_type, c) # Registration is deferred until the first instance of class is created diff --git a/tests/test_python.py b/tests/test_python.py index 80791fab..c66ea001 100644 --- a/tests/test_python.py +++ b/tests/test_python.py @@ -1685,6 +1685,16 @@ class Extension(model.extensible_class): == expect ) + # Test that concrete classes derived from abstract classes can be iterated + expect = set() + assert ( + set(objset.foreach_type(model.abstract_class, match_subclass=False)) == expect + ) + + expect.add(objset.find_by_id("http://serialize.example.com/concrete-class")) + assert expect != {None} + assert set(objset.foreach_type(model.abstract_class, match_subclass=True)) == expect + @pytest.mark.parametrize( "abstract,concrete", From ee8d29f2400c7555051223c2eff6676a341a3960 Mon Sep 17 00:00:00 2001 From: Joshua Watt Date: Fri, 14 Jun 2024 08:12:10 -0600 Subject: [PATCH 04/14] python: Optimize property lookup Optimizes the way properties are looked up in the generated python code: 1. Directly access "_obj_" properties using self.__dict__. This allow the expensive(*) startswith() to be removed from __getattr__() and __setattr__() 2. Move the property lookup first in __getattr__(). This is the most common operation, so have it occur first can help will lookup speed 3. Intern the python name and IRI of all properties * startswith() is expensive because it can't be interned --- src/shacl2code/lang/templates/python.j2 | 81 ++++++++++++------------- tests/expect/python/test-context.py | 81 ++++++++++++------------- tests/expect/python/test.py | 81 ++++++++++++------------- 3 files changed, 117 insertions(+), 126 deletions(-) diff --git a/src/shacl2code/lang/templates/python.j2 b/src/shacl2code/lang/templates/python.j2 index 13dc2234..a86101a9 100644 --- a/src/shacl2code/lang/templates/python.j2 +++ b/src/shacl2code/lang/templates/python.j2 @@ -10,8 +10,9 @@ import functools import hashlib import json import re -import time +import sys import threading +import time from contextlib import contextmanager from datetime import datetime, timezone, timedelta from enum import Enum @@ -549,11 +550,11 @@ class SHACLObject(object): cls._register_props() cls._NEEDS_REG = False - self._obj_data = {} - self._obj_metadata = {} + self.__dict__["_obj_data"] = {} + self.__dict__["_obj_metadata"] = {} for iri, prop, _, _, _, _ in self.__iter_props(): - self._obj_data[iri] = prop.init() + self.__dict__["_obj_data"][iri] = prop.init() for k, v in kwargs.items(): setattr(self, k, v) @@ -580,15 +581,16 @@ class SHACLObject(object): while hasattr(cls, pyname): pyname = pyname + "_" + pyname = sys.intern(pyname) + iri = sys.intern(iri) + cls._OBJ_IRIS[pyname] = iri cls._OBJ_PROPERTIES[iri] = (prop, min_count, max_count, pyname, compact) def __setattr__(self, name, value): - if name.startswith("_obj_"): - return super().__setattr__(name, value) - if name == self.ID_ALIAS: - name = "_id" + self["@id"] = value + return try: iri = self._OBJ_IRIS[name] @@ -599,35 +601,32 @@ class SHACLObject(object): ) def __getattr__(self, name): - if name.startswith("_obj_"): - return self.__dict__[name] + if name in self._OBJ_IRIS: + return self.__dict__["_obj_data"][self._OBJ_IRIS[name]] + + if name == self.ID_ALIAS: + return self.__dict__["_obj_data"]["@id"] if name == "_metadata": - return self._obj_metadata + return self.__dict__["_obj_metadata"] if name == "_IRI": return self._OBJ_IRIS - if name == self.ID_ALIAS: - name = "_id" - if name == "TYPE": return self.__class__._OBJ_TYPE if name == "COMPACT_TYPE": return self.__class__._OBJ_COMPACT_TYPE - try: - iri = self._OBJ_IRIS[name] - return self[iri] - except KeyError: - raise AttributeError( - f"'{name}' is not a valid property of {self.__class__.__name__}" - ) + raise AttributeError( + f"'{name}' is not a valid property of {self.__class__.__name__}" + ) def __delattr__(self, name): if name == self.ID_ALIAS: - name = "_id" + del self["@id"] + return try: iri = self._OBJ_IRIS[name] @@ -650,7 +649,7 @@ class SHACLObject(object): yield iri, *v def __getitem__(self, iri): - return self._obj_data[iri] + return self.__dict__["_obj_data"][iri] def __setitem__(self, iri, value): if iri == "@id": @@ -672,11 +671,11 @@ class SHACLObject(object): prop, _, _, _, _ = self.__get_prop(iri) prop.validate(value) - self._obj_data[iri] = prop.set(value) + self.__dict__["_obj_data"][iri] = prop.set(value) def __delitem__(self, iri): prop, _, _, _, _ = self.__get_prop(iri) - self._obj_data[iri] = prop.init() + self.__dict__["_obj_data"][iri] = prop.init() def __iter__(self): return self._OBJ_PROPERTIES.keys() @@ -694,7 +693,7 @@ class SHACLObject(object): if callback(self, path): for iri, prop, _, _, _, _ in self.__iter_props(): - prop.walk(self._obj_data[iri], callback, path + [f".{iri}"]) + prop.walk(self.__dict__["_obj_data"][iri], callback, path + [f".{iri}"]) def property_keys(self): for iri, _, _, _, pyname, compact in self.__iter_props(): @@ -711,7 +710,7 @@ class SHACLObject(object): for iri, prop, _, _, _, _ in self.__iter_props(): for c in prop.iter_objects( - self._obj_data[iri], recursive=recursive, visited=visited + self.__dict__["_obj_data"][iri], recursive=recursive, visited=visited ): yield c @@ -737,7 +736,7 @@ class SHACLObject(object): def _encode_properties(self, encoder, state): for iri, prop, min_count, max_count, pyname, compact in self.__iter_props(): - value = self._obj_data[iri] + value = self.__dict__["_obj_data"][iri] if prop.elide(value): if min_count: raise ValueError( @@ -824,7 +823,7 @@ class SHACLObject(object): with decoder.read_property(read_key) as prop_d: v = prop.decode(prop_d, objectset=objectset) prop.validate(v) - self._obj_data[iri] = v + self.__dict__["_obj_data"][iri] = v return True return False @@ -836,8 +835,8 @@ class SHACLObject(object): visited.add(self) for iri, prop, _, _, _, _ in self.__iter_props(): - self._obj_data[iri] = prop.link_prop( - self._obj_data[iri], + self.__dict__["_obj_data"][iri] = prop.link_prop( + self.__dict__["_obj_data"][iri], objectset, missing, visited, @@ -878,9 +877,9 @@ class SHACLExtensibleObject(object): def __init__(self, typ=None, **kwargs): super().__init__(**kwargs) if typ: - self._obj_TYPE = (typ, None) + self.__dict__["_obj_TYPE"] = (typ, None) else: - self._obj_TYPE = (self._OBJ_TYPE, self._OBJ_COMPACT_TYPE) + self.__dict__["_obj_TYPE"] = (self._OBJ_TYPE, self._OBJ_COMPACT_TYPE) @classmethod def _make_object(cls, typ): @@ -906,7 +905,7 @@ class SHACLExtensibleObject(object): ) with decoder.read_property(key) as prop_d: - self._obj_data[key] = prop_d.read_value() + self.__dict__["_obj_data"][key] = prop_d.read_value() def _encode_properties(self, encoder, state): def encode_value(encoder, v): @@ -927,7 +926,7 @@ class SHACLExtensibleObject(object): if self.CLOSED: return - for iri, value in self._obj_data.items(): + for iri, value in self.__dict__["_obj_data"].items(): if iri in self._OBJ_PROPERTIES: continue @@ -943,7 +942,7 @@ class SHACLExtensibleObject(object): if not is_IRI(iri): raise KeyError(f"Key '{iri}' must be an IRI") - self._obj_data[iri] = value + self.__dict__["_obj_data"][iri] = value def __delitem__(self, iri): try: @@ -954,13 +953,13 @@ class SHACLExtensibleObject(object): if not is_IRI(iri): raise KeyError(f"Key '{iri}' must be an IRI") - del self._obj_data[iri] + del self.__dict__["_obj_data"][iri] def __getattr__(self, name): if name == "TYPE": - return self._obj_TYPE[0] + return self.__dict__["_obj_TYPE"][0] if name == "COMPACT_TYPE": - return self._obj_TYPE[1] + return self.__dict__["_obj_TYPE"][1] return super().__getattr__(name) def property_keys(self): @@ -972,7 +971,7 @@ class SHACLExtensibleObject(object): if self.CLOSED: return - for iri in self._obj_data.keys(): + for iri in self.__dict__["_obj_data"].keys(): if iri not in iris: yield None, iri, None @@ -2052,6 +2051,4 @@ def main(): if __name__ == "__main__": - import sys - sys.exit(main()) diff --git a/tests/expect/python/test-context.py b/tests/expect/python/test-context.py index fc663985..a205e088 100755 --- a/tests/expect/python/test-context.py +++ b/tests/expect/python/test-context.py @@ -10,8 +10,9 @@ import hashlib import json import re -import time +import sys import threading +import time from contextlib import contextmanager from datetime import datetime, timezone, timedelta from enum import Enum @@ -549,11 +550,11 @@ def __init__(self, **kwargs): cls._register_props() cls._NEEDS_REG = False - self._obj_data = {} - self._obj_metadata = {} + self.__dict__["_obj_data"] = {} + self.__dict__["_obj_metadata"] = {} for iri, prop, _, _, _, _ in self.__iter_props(): - self._obj_data[iri] = prop.init() + self.__dict__["_obj_data"][iri] = prop.init() for k, v in kwargs.items(): setattr(self, k, v) @@ -580,15 +581,16 @@ def _add_property( while hasattr(cls, pyname): pyname = pyname + "_" + pyname = sys.intern(pyname) + iri = sys.intern(iri) + cls._OBJ_IRIS[pyname] = iri cls._OBJ_PROPERTIES[iri] = (prop, min_count, max_count, pyname, compact) def __setattr__(self, name, value): - if name.startswith("_obj_"): - return super().__setattr__(name, value) - if name == self.ID_ALIAS: - name = "_id" + self["@id"] = value + return try: iri = self._OBJ_IRIS[name] @@ -599,35 +601,32 @@ def __setattr__(self, name, value): ) def __getattr__(self, name): - if name.startswith("_obj_"): - return self.__dict__[name] + if name in self._OBJ_IRIS: + return self.__dict__["_obj_data"][self._OBJ_IRIS[name]] + + if name == self.ID_ALIAS: + return self.__dict__["_obj_data"]["@id"] if name == "_metadata": - return self._obj_metadata + return self.__dict__["_obj_metadata"] if name == "_IRI": return self._OBJ_IRIS - if name == self.ID_ALIAS: - name = "_id" - if name == "TYPE": return self.__class__._OBJ_TYPE if name == "COMPACT_TYPE": return self.__class__._OBJ_COMPACT_TYPE - try: - iri = self._OBJ_IRIS[name] - return self[iri] - except KeyError: - raise AttributeError( - f"'{name}' is not a valid property of {self.__class__.__name__}" - ) + raise AttributeError( + f"'{name}' is not a valid property of {self.__class__.__name__}" + ) def __delattr__(self, name): if name == self.ID_ALIAS: - name = "_id" + del self["@id"] + return try: iri = self._OBJ_IRIS[name] @@ -650,7 +649,7 @@ def __iter_props(self): yield iri, *v def __getitem__(self, iri): - return self._obj_data[iri] + return self.__dict__["_obj_data"][iri] def __setitem__(self, iri, value): if iri == "@id": @@ -672,11 +671,11 @@ def __setitem__(self, iri, value): prop, _, _, _, _ = self.__get_prop(iri) prop.validate(value) - self._obj_data[iri] = prop.set(value) + self.__dict__["_obj_data"][iri] = prop.set(value) def __delitem__(self, iri): prop, _, _, _, _ = self.__get_prop(iri) - self._obj_data[iri] = prop.init() + self.__dict__["_obj_data"][iri] = prop.init() def __iter__(self): return self._OBJ_PROPERTIES.keys() @@ -694,7 +693,7 @@ def callback(object, path): if callback(self, path): for iri, prop, _, _, _, _ in self.__iter_props(): - prop.walk(self._obj_data[iri], callback, path + [f".{iri}"]) + prop.walk(self.__dict__["_obj_data"][iri], callback, path + [f".{iri}"]) def property_keys(self): for iri, _, _, _, pyname, compact in self.__iter_props(): @@ -711,7 +710,7 @@ def iter_objects(self, *, recursive=False, visited=None): for iri, prop, _, _, _, _ in self.__iter_props(): for c in prop.iter_objects( - self._obj_data[iri], recursive=recursive, visited=visited + self.__dict__["_obj_data"][iri], recursive=recursive, visited=visited ): yield c @@ -737,7 +736,7 @@ def encode(self, encoder, state): def _encode_properties(self, encoder, state): for iri, prop, min_count, max_count, pyname, compact in self.__iter_props(): - value = self._obj_data[iri] + value = self.__dict__["_obj_data"][iri] if prop.elide(value): if min_count: raise ValueError( @@ -824,7 +823,7 @@ def _decode_prop(self, decoder, key, objectset=None): with decoder.read_property(read_key) as prop_d: v = prop.decode(prop_d, objectset=objectset) prop.validate(v) - self._obj_data[iri] = v + self.__dict__["_obj_data"][iri] = v return True return False @@ -836,8 +835,8 @@ def link_helper(self, objectset, missing, visited): visited.add(self) for iri, prop, _, _, _, _ in self.__iter_props(): - self._obj_data[iri] = prop.link_prop( - self._obj_data[iri], + self.__dict__["_obj_data"][iri] = prop.link_prop( + self.__dict__["_obj_data"][iri], objectset, missing, visited, @@ -878,9 +877,9 @@ class SHACLExtensibleObject(object): def __init__(self, typ=None, **kwargs): super().__init__(**kwargs) if typ: - self._obj_TYPE = (typ, None) + self.__dict__["_obj_TYPE"] = (typ, None) else: - self._obj_TYPE = (self._OBJ_TYPE, self._OBJ_COMPACT_TYPE) + self.__dict__["_obj_TYPE"] = (self._OBJ_TYPE, self._OBJ_COMPACT_TYPE) @classmethod def _make_object(cls, typ): @@ -906,7 +905,7 @@ def _decode_properties(self, decoder, objectset=None): ) with decoder.read_property(key) as prop_d: - self._obj_data[key] = prop_d.read_value() + self.__dict__["_obj_data"][key] = prop_d.read_value() def _encode_properties(self, encoder, state): def encode_value(encoder, v): @@ -927,7 +926,7 @@ def encode_value(encoder, v): if self.CLOSED: return - for iri, value in self._obj_data.items(): + for iri, value in self.__dict__["_obj_data"].items(): if iri in self._OBJ_PROPERTIES: continue @@ -943,7 +942,7 @@ def __setitem__(self, iri, value): if not is_IRI(iri): raise KeyError(f"Key '{iri}' must be an IRI") - self._obj_data[iri] = value + self.__dict__["_obj_data"][iri] = value def __delitem__(self, iri): try: @@ -954,13 +953,13 @@ def __delitem__(self, iri): if not is_IRI(iri): raise KeyError(f"Key '{iri}' must be an IRI") - del self._obj_data[iri] + del self.__dict__["_obj_data"][iri] def __getattr__(self, name): if name == "TYPE": - return self._obj_TYPE[0] + return self.__dict__["_obj_TYPE"][0] if name == "COMPACT_TYPE": - return self._obj_TYPE[1] + return self.__dict__["_obj_TYPE"][1] return super().__getattr__(name) def property_keys(self): @@ -972,7 +971,7 @@ def property_keys(self): if self.CLOSED: return - for iri in self._obj_data.keys(): + for iri in self.__dict__["_obj_data"].keys(): if iri not in iris: yield None, iri, None @@ -2441,6 +2440,4 @@ def main(): if __name__ == "__main__": - import sys - sys.exit(main()) diff --git a/tests/expect/python/test.py b/tests/expect/python/test.py index 771e2d94..2c9f082a 100644 --- a/tests/expect/python/test.py +++ b/tests/expect/python/test.py @@ -10,8 +10,9 @@ import hashlib import json import re -import time +import sys import threading +import time from contextlib import contextmanager from datetime import datetime, timezone, timedelta from enum import Enum @@ -549,11 +550,11 @@ def __init__(self, **kwargs): cls._register_props() cls._NEEDS_REG = False - self._obj_data = {} - self._obj_metadata = {} + self.__dict__["_obj_data"] = {} + self.__dict__["_obj_metadata"] = {} for iri, prop, _, _, _, _ in self.__iter_props(): - self._obj_data[iri] = prop.init() + self.__dict__["_obj_data"][iri] = prop.init() for k, v in kwargs.items(): setattr(self, k, v) @@ -580,15 +581,16 @@ def _add_property( while hasattr(cls, pyname): pyname = pyname + "_" + pyname = sys.intern(pyname) + iri = sys.intern(iri) + cls._OBJ_IRIS[pyname] = iri cls._OBJ_PROPERTIES[iri] = (prop, min_count, max_count, pyname, compact) def __setattr__(self, name, value): - if name.startswith("_obj_"): - return super().__setattr__(name, value) - if name == self.ID_ALIAS: - name = "_id" + self["@id"] = value + return try: iri = self._OBJ_IRIS[name] @@ -599,35 +601,32 @@ def __setattr__(self, name, value): ) def __getattr__(self, name): - if name.startswith("_obj_"): - return self.__dict__[name] + if name in self._OBJ_IRIS: + return self.__dict__["_obj_data"][self._OBJ_IRIS[name]] + + if name == self.ID_ALIAS: + return self.__dict__["_obj_data"]["@id"] if name == "_metadata": - return self._obj_metadata + return self.__dict__["_obj_metadata"] if name == "_IRI": return self._OBJ_IRIS - if name == self.ID_ALIAS: - name = "_id" - if name == "TYPE": return self.__class__._OBJ_TYPE if name == "COMPACT_TYPE": return self.__class__._OBJ_COMPACT_TYPE - try: - iri = self._OBJ_IRIS[name] - return self[iri] - except KeyError: - raise AttributeError( - f"'{name}' is not a valid property of {self.__class__.__name__}" - ) + raise AttributeError( + f"'{name}' is not a valid property of {self.__class__.__name__}" + ) def __delattr__(self, name): if name == self.ID_ALIAS: - name = "_id" + del self["@id"] + return try: iri = self._OBJ_IRIS[name] @@ -650,7 +649,7 @@ def __iter_props(self): yield iri, *v def __getitem__(self, iri): - return self._obj_data[iri] + return self.__dict__["_obj_data"][iri] def __setitem__(self, iri, value): if iri == "@id": @@ -672,11 +671,11 @@ def __setitem__(self, iri, value): prop, _, _, _, _ = self.__get_prop(iri) prop.validate(value) - self._obj_data[iri] = prop.set(value) + self.__dict__["_obj_data"][iri] = prop.set(value) def __delitem__(self, iri): prop, _, _, _, _ = self.__get_prop(iri) - self._obj_data[iri] = prop.init() + self.__dict__["_obj_data"][iri] = prop.init() def __iter__(self): return self._OBJ_PROPERTIES.keys() @@ -694,7 +693,7 @@ def callback(object, path): if callback(self, path): for iri, prop, _, _, _, _ in self.__iter_props(): - prop.walk(self._obj_data[iri], callback, path + [f".{iri}"]) + prop.walk(self.__dict__["_obj_data"][iri], callback, path + [f".{iri}"]) def property_keys(self): for iri, _, _, _, pyname, compact in self.__iter_props(): @@ -711,7 +710,7 @@ def iter_objects(self, *, recursive=False, visited=None): for iri, prop, _, _, _, _ in self.__iter_props(): for c in prop.iter_objects( - self._obj_data[iri], recursive=recursive, visited=visited + self.__dict__["_obj_data"][iri], recursive=recursive, visited=visited ): yield c @@ -737,7 +736,7 @@ def encode(self, encoder, state): def _encode_properties(self, encoder, state): for iri, prop, min_count, max_count, pyname, compact in self.__iter_props(): - value = self._obj_data[iri] + value = self.__dict__["_obj_data"][iri] if prop.elide(value): if min_count: raise ValueError( @@ -824,7 +823,7 @@ def _decode_prop(self, decoder, key, objectset=None): with decoder.read_property(read_key) as prop_d: v = prop.decode(prop_d, objectset=objectset) prop.validate(v) - self._obj_data[iri] = v + self.__dict__["_obj_data"][iri] = v return True return False @@ -836,8 +835,8 @@ def link_helper(self, objectset, missing, visited): visited.add(self) for iri, prop, _, _, _, _ in self.__iter_props(): - self._obj_data[iri] = prop.link_prop( - self._obj_data[iri], + self.__dict__["_obj_data"][iri] = prop.link_prop( + self.__dict__["_obj_data"][iri], objectset, missing, visited, @@ -878,9 +877,9 @@ class SHACLExtensibleObject(object): def __init__(self, typ=None, **kwargs): super().__init__(**kwargs) if typ: - self._obj_TYPE = (typ, None) + self.__dict__["_obj_TYPE"] = (typ, None) else: - self._obj_TYPE = (self._OBJ_TYPE, self._OBJ_COMPACT_TYPE) + self.__dict__["_obj_TYPE"] = (self._OBJ_TYPE, self._OBJ_COMPACT_TYPE) @classmethod def _make_object(cls, typ): @@ -906,7 +905,7 @@ def _decode_properties(self, decoder, objectset=None): ) with decoder.read_property(key) as prop_d: - self._obj_data[key] = prop_d.read_value() + self.__dict__["_obj_data"][key] = prop_d.read_value() def _encode_properties(self, encoder, state): def encode_value(encoder, v): @@ -927,7 +926,7 @@ def encode_value(encoder, v): if self.CLOSED: return - for iri, value in self._obj_data.items(): + for iri, value in self.__dict__["_obj_data"].items(): if iri in self._OBJ_PROPERTIES: continue @@ -943,7 +942,7 @@ def __setitem__(self, iri, value): if not is_IRI(iri): raise KeyError(f"Key '{iri}' must be an IRI") - self._obj_data[iri] = value + self.__dict__["_obj_data"][iri] = value def __delitem__(self, iri): try: @@ -954,13 +953,13 @@ def __delitem__(self, iri): if not is_IRI(iri): raise KeyError(f"Key '{iri}' must be an IRI") - del self._obj_data[iri] + del self.__dict__["_obj_data"][iri] def __getattr__(self, name): if name == "TYPE": - return self._obj_TYPE[0] + return self.__dict__["_obj_TYPE"][0] if name == "COMPACT_TYPE": - return self._obj_TYPE[1] + return self.__dict__["_obj_TYPE"][1] return super().__getattr__(name) def property_keys(self): @@ -972,7 +971,7 @@ def property_keys(self): if self.CLOSED: return - for iri in self._obj_data.keys(): + for iri in self.__dict__["_obj_data"].keys(): if iri not in iris: yield None, iri, None @@ -2404,6 +2403,4 @@ def main(): if __name__ == "__main__": - import sys - sys.exit(main()) From 08a2dadb5f6f6bbcfcb187502c3cbccee9baae1e Mon Sep 17 00:00:00 2001 From: Joshua Watt Date: Fri, 14 Jun 2024 14:55:28 -0600 Subject: [PATCH 05/14] Bump version for release --- src/shacl2code/version.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/shacl2code/version.py b/src/shacl2code/version.py index a1d5a2f3..f8c83ce8 100644 --- a/src/shacl2code/version.py +++ b/src/shacl2code/version.py @@ -1 +1 @@ -VERSION = "0.0.10" +VERSION = "0.0.11" From 92d0500d3647aa067b16d6f33ee905ca3402fb5a Mon Sep 17 00:00:00 2001 From: Joshua Watt Date: Fri, 14 Jun 2024 16:20:45 -0600 Subject: [PATCH 06/14] jsonschema: Add support for shortened named individuals If a named individual is shortened by the context, it may not be a valid IRI. Add explicit matches for all of the context-renamed named individuals that could be used in place of an object. Note that only context manipulated named individuals need to be checked, since full IRI name individuals are the same as any other IRI reference. --- src/shacl2code/lang/templates/jsonschema.j2 | 10 +++++++++- tests/data/model/test-context.json | 1 + tests/data/model/test.ttl | 4 ++++ tests/expect/jsonschema/test-context.json | 5 +++++ tests/expect/python/test-context.py | 2 ++ tests/expect/python/test.py | 2 ++ tests/expect/raw/test.txt | 2 +- tests/test_jsonschema.py | 4 ++++ 8 files changed, 28 insertions(+), 2 deletions(-) diff --git a/src/shacl2code/lang/templates/jsonschema.j2 b/src/shacl2code/lang/templates/jsonschema.j2 index 68edbb77..91cda97a 100644 --- a/src/shacl2code/lang/templates/jsonschema.j2 +++ b/src/shacl2code/lang/templates/jsonschema.j2 @@ -86,9 +86,14 @@ }, {%- endif %} "{{ varname(*class.clsname) }}_derived": { - {%- set ns = namespace(external_ref=False, local_ref=False, any_ref=False, json_refs=[]) %} + {%- set ns = namespace(json_refs=[], named_individuals=[]) %} {%- for d in get_all_derived(class) + [class._id] %} {%- set ns.json_refs = ns.json_refs + ["#/$defs/" + varname(*classes.get(d).clsname)] %} + {%- for n in classes.get(d).named_individuals %} + {%- if context.compact(n._id) != n._id %} + {%- set ns.named_individuals = ns.named_individuals + [context.compact(n._id)] %} + {%- endif %} + {%- endfor %} {%- endfor %} "anyOf": [ {%- if ns.json_refs %} @@ -104,6 +109,9 @@ ] }, {%- endif %} + {%- for n in ns.named_individuals %} + { "const": "{{ n }}" }, + {%- endfor %} { "$ref": "#/$defs/BlankNodeOrIRI" } ] }, diff --git a/tests/data/model/test-context.json b/tests/data/model/test-context.json index bb0166bd..abf3692f 100644 --- a/tests/data/model/test-context.json +++ b/tests/data/model/test-context.json @@ -87,6 +87,7 @@ "@id": "test:test-class/enum-prop-no-class", "@type": "@id" }, + "test-class/named": "test:test-class/named", "test-class/regex": { "@id": "test:test-class/regex", "@type": "xsd:string" diff --git a/tests/data/model/test.ttl b/tests/data/model/test.ttl index 37a195ab..f6dc4e3c 100644 --- a/tests/data/model/test.ttl +++ b/tests/data/model/test.ttl @@ -166,6 +166,10 @@ ] . + a owl:NamedIndividual, ; + rdfs:label "A named individual of the test class" + . + a sh:NodeShape, owl:Class ; rdfs:subClassOf ; sh:property [ diff --git a/tests/expect/jsonschema/test-context.json b/tests/expect/jsonschema/test-context.json index 7f815a3c..73b62e9d 100644 --- a/tests/expect/jsonschema/test-context.json +++ b/tests/expect/jsonschema/test-context.json @@ -178,6 +178,9 @@ { "$ref": "#/$defs/enumType" } ] }, + { "const": "enumType/foo" }, + { "const": "enumType/bar" }, + { "const": "enumType/nolabel" }, { "$ref": "#/$defs/BlankNodeOrIRI" } ] }, @@ -564,6 +567,7 @@ { "$ref": "#/$defs/parentclass" } ] }, + { "const": "test-class/named" }, { "$ref": "#/$defs/BlankNodeOrIRI" } ] }, @@ -689,6 +693,7 @@ { "$ref": "#/$defs/testclass" } ] }, + { "const": "test-class/named" }, { "$ref": "#/$defs/BlankNodeOrIRI" } ] }, diff --git a/tests/expect/python/test-context.py b/tests/expect/python/test-context.py index a205e088..0ab47d3b 100755 --- a/tests/expect/python/test-context.py +++ b/tests/expect/python/test-context.py @@ -2114,7 +2114,9 @@ class test_another_class(SHACLObject): class test_class(parent_class): NODE_KIND = NodeKind.BlankNodeOrIRI NAMED_INDIVIDUALS = { + "named": "http://example.org/test-class/named", } + named = "http://example.org/test-class/named" @classmethod def _register_props(cls): diff --git a/tests/expect/python/test.py b/tests/expect/python/test.py index 2c9f082a..701d21fc 100644 --- a/tests/expect/python/test.py +++ b/tests/expect/python/test.py @@ -2108,7 +2108,9 @@ class http_example_org_test_another_class(SHACLObject): class http_example_org_test_class(http_example_org_parent_class): NODE_KIND = NodeKind.BlankNodeOrIRI NAMED_INDIVIDUALS = { + "named": "http://example.org/test-class/named", } + named = "http://example.org/test-class/named" @classmethod def _register_props(cls): diff --git a/tests/expect/raw/test.txt b/tests/expect/raw/test.txt index d9f9c471..70c83b29 100644 --- a/tests/expect/raw/test.txt +++ b/tests/expect/raw/test.txt @@ -16,7 +16,7 @@ Class(_id='http://example.org/non-shape-class', clsname=['http', '//example.org/ Class(_id='http://example.org/parent-class', clsname=['http', '//example.org/parent-class'], parent_ids=[], derived_ids=['http://example.org/aaa-derived-class', 'http://example.org/test-class'], properties=[], comment='The parent class', id_property=None, node_kind=rdflib.term.URIRef('http://www.w3.org/ns/shacl#BlankNodeOrIRI'), is_extensible=False, is_abstract=False, named_individuals=[]) Class(_id='http://example.org/required-abstract', clsname=['http', '//example.org/required-abstract'], parent_ids=[], derived_ids=[], properties=[Property(path='http://example.org/required-abstract/abstract-class-prop', varname='abstract-class-prop', comment='A required abstract class property', max_count=1, min_count=1, enum_values=None, class_id='http://example.org/abstract-class', datatype='', pattern='')], comment='A class with a mandatory abstract class', id_property=None, node_kind=rdflib.term.URIRef('http://www.w3.org/ns/shacl#BlankNodeOrIRI'), is_extensible=False, is_abstract=False, named_individuals=[]) Class(_id='http://example.org/test-another-class', clsname=['http', '//example.org/test-another-class'], parent_ids=[], derived_ids=[], properties=[], comment='Another class', id_property=None, node_kind=rdflib.term.URIRef('http://www.w3.org/ns/shacl#BlankNodeOrIRI'), is_extensible=False, is_abstract=False, named_individuals=[]) -Class(_id='http://example.org/test-class', clsname=['http', '//example.org/test-class'], parent_ids=['http://example.org/parent-class'], derived_ids=['http://example.org/test-class-required', 'http://example.org/test-derived-class'], properties=[Property(path='http://example.org/encode', varname='encode', comment='A property that conflicts with an existing SHACLObject property', max_count=1, min_count=None, enum_values=None, class_id='', datatype='http://www.w3.org/2001/XMLSchema#string', pattern=''), Property(path='http://example.org/import', varname='import', comment='A property that is a keyword', max_count=1, min_count=None, enum_values=None, class_id='', datatype='http://www.w3.org/2001/XMLSchema#string', pattern=''), Property(path='http://example.org/test-class/anyuri-prop', varname='anyuri-prop', comment='a URI', max_count=1, min_count=None, enum_values=None, class_id='', datatype='http://www.w3.org/2001/XMLSchema#anyURI', pattern=''), Property(path='http://example.org/test-class/boolean-prop', varname='boolean-prop', comment='a boolean property', max_count=1, min_count=None, enum_values=None, class_id='', datatype='http://www.w3.org/2001/XMLSchema#boolean', pattern=''), Property(path='http://example.org/test-class/class-list-prop', varname='class-list-prop', comment='A test-class list property', max_count=None, min_count=None, enum_values=None, class_id='http://example.org/test-class', datatype='', pattern=''), Property(path='http://example.org/test-class/class-prop', varname='class-prop', comment='A test-class property', max_count=1, min_count=None, enum_values=None, class_id='http://example.org/test-class', datatype='', pattern=''), Property(path='http://example.org/test-class/class-prop-no-class', varname='class-prop-no-class', comment='A test-class property with no sh:class', max_count=1, min_count=None, enum_values=None, class_id='http://example.org/test-class', datatype='', pattern=''), Property(path='http://example.org/test-class/datetime-list-prop', varname='datetime-list-prop', comment='A datetime list property', max_count=None, min_count=None, enum_values=None, class_id='', datatype='http://www.w3.org/2001/XMLSchema#dateTime', pattern=''), Property(path='http://example.org/test-class/datetime-scalar-prop', varname='datetime-scalar-prop', comment='A scalar datetime property', max_count=1, min_count=None, enum_values=None, class_id='', datatype='http://www.w3.org/2001/XMLSchema#dateTime', pattern=''), Property(path='http://example.org/test-class/datetimestamp-scalar-prop', varname='datetimestamp-scalar-prop', comment='A scalar dateTimeStamp property', max_count=1, min_count=None, enum_values=None, class_id='', datatype='http://www.w3.org/2001/XMLSchema#dateTimeStamp', pattern=''), Property(path='http://example.org/test-class/enum-list-prop', varname='enum-list-prop', comment='A enum list property', max_count=None, min_count=None, enum_values=[rdflib.term.URIRef('http://example.org/enumType/bar'), rdflib.term.URIRef('http://example.org/enumType/foo'), rdflib.term.URIRef('http://example.org/enumType/nolabel'), rdflib.term.URIRef('http://example.org/enumType/non-named-individual')], class_id='http://example.org/enumType', datatype='', pattern=''), Property(path='http://example.org/test-class/enum-prop', varname='enum-prop', comment='A enum property', max_count=1, min_count=None, enum_values=[rdflib.term.URIRef('http://example.org/enumType/bar'), rdflib.term.URIRef('http://example.org/enumType/foo'), rdflib.term.URIRef('http://example.org/enumType/nolabel'), rdflib.term.URIRef('http://example.org/enumType/non-named-individual')], class_id='http://example.org/enumType', datatype='', pattern=''), Property(path='http://example.org/test-class/enum-prop-no-class', varname='enum-prop-no-class', comment='A enum property with no sh:class', max_count=1, min_count=None, enum_values=[rdflib.term.URIRef('http://example.org/enumType/bar'), rdflib.term.URIRef('http://example.org/enumType/foo'), rdflib.term.URIRef('http://example.org/enumType/nolabel'), rdflib.term.URIRef('http://example.org/enumType/non-named-individual')], class_id='http://example.org/enumType', datatype='', pattern=''), Property(path='http://example.org/test-class/float-prop', varname='float-prop', comment='a float property', max_count=1, min_count=None, enum_values=None, class_id='', datatype='http://www.w3.org/2001/XMLSchema#decimal', pattern=''), Property(path='http://example.org/test-class/integer-prop', varname='integer-prop', comment='a non-negative integer', max_count=1, min_count=None, enum_values=None, class_id='', datatype='http://www.w3.org/2001/XMLSchema#integer', pattern=''), Property(path='http://example.org/test-class/named-property', varname=rdflib.term.Literal('named_property'), comment='A named property', max_count=1, min_count=None, enum_values=None, class_id='', datatype='http://www.w3.org/2001/XMLSchema#string', pattern=''), Property(path='http://example.org/test-class/non-shape', varname='non-shape', comment='A class with no shape', max_count=1, min_count=None, enum_values=None, class_id='http://example.org/non-shape-class', datatype='', pattern=''), Property(path='http://example.org/test-class/nonnegative-integer-prop', varname='nonnegative-integer-prop', comment='a non-negative integer', max_count=1, min_count=None, enum_values=None, class_id='', datatype='http://www.w3.org/2001/XMLSchema#nonNegativeInteger', pattern=''), Property(path='http://example.org/test-class/positive-integer-prop', varname='positive-integer-prop', comment='A positive integer', max_count=1, min_count=None, enum_values=None, class_id='', datatype='http://www.w3.org/2001/XMLSchema#positiveInteger', pattern=''), Property(path='http://example.org/test-class/regex', varname='regex', comment='A regex validated string', max_count=1, min_count=None, enum_values=None, class_id='', datatype='http://www.w3.org/2001/XMLSchema#string', pattern='^foo\\d'), Property(path='http://example.org/test-class/regex-datetime', varname='regex-datetime', comment='A regex dateTime', max_count=1, min_count=None, enum_values=None, class_id='', datatype='http://www.w3.org/2001/XMLSchema#dateTime', pattern='^\\d\\d\\d\\d-\\d\\d-\\d\\dT\\d\\d:\\d\\d:\\d\\d\\+01:00$'), Property(path='http://example.org/test-class/regex-datetimestamp', varname='regex-datetimestamp', comment='A regex dateTimeStamp', max_count=1, min_count=None, enum_values=None, class_id='', datatype='http://www.w3.org/2001/XMLSchema#dateTimeStamp', pattern='^\\d\\d\\d\\d-\\d\\d-\\d\\dT\\d\\d:\\d\\d:\\d\\dZ$'), Property(path='http://example.org/test-class/regex-list', varname='regex-list', comment='A regex validated string list', max_count=None, min_count=None, enum_values=None, class_id='', datatype='http://www.w3.org/2001/XMLSchema#string', pattern='^foo\\d'), Property(path='http://example.org/test-class/string-list-no-datatype', varname='string-list-no-datatype', comment='A string list property with no sh:datatype', max_count=None, min_count=None, enum_values=None, class_id='', datatype='http://www.w3.org/2001/XMLSchema#string', pattern=''), Property(path='http://example.org/test-class/string-list-prop', varname='string-list-prop', comment='A string list property', max_count=None, min_count=None, enum_values=None, class_id='', datatype='http://www.w3.org/2001/XMLSchema#string', pattern=''), Property(path='http://example.org/test-class/string-scalar-prop', varname='string-scalar-prop', comment='A scalar string propery', max_count=1, min_count=None, enum_values=None, class_id='', datatype='http://www.w3.org/2001/XMLSchema#string', pattern='')], comment='The test class', id_property=None, node_kind=rdflib.term.URIRef('http://www.w3.org/ns/shacl#BlankNodeOrIRI'), is_extensible=False, is_abstract=False, named_individuals=[]) +Class(_id='http://example.org/test-class', clsname=['http', '//example.org/test-class'], parent_ids=['http://example.org/parent-class'], derived_ids=['http://example.org/test-class-required', 'http://example.org/test-derived-class'], properties=[Property(path='http://example.org/encode', varname='encode', comment='A property that conflicts with an existing SHACLObject property', max_count=1, min_count=None, enum_values=None, class_id='', datatype='http://www.w3.org/2001/XMLSchema#string', pattern=''), Property(path='http://example.org/import', varname='import', comment='A property that is a keyword', max_count=1, min_count=None, enum_values=None, class_id='', datatype='http://www.w3.org/2001/XMLSchema#string', pattern=''), Property(path='http://example.org/test-class/anyuri-prop', varname='anyuri-prop', comment='a URI', max_count=1, min_count=None, enum_values=None, class_id='', datatype='http://www.w3.org/2001/XMLSchema#anyURI', pattern=''), Property(path='http://example.org/test-class/boolean-prop', varname='boolean-prop', comment='a boolean property', max_count=1, min_count=None, enum_values=None, class_id='', datatype='http://www.w3.org/2001/XMLSchema#boolean', pattern=''), Property(path='http://example.org/test-class/class-list-prop', varname='class-list-prop', comment='A test-class list property', max_count=None, min_count=None, enum_values=None, class_id='http://example.org/test-class', datatype='', pattern=''), Property(path='http://example.org/test-class/class-prop', varname='class-prop', comment='A test-class property', max_count=1, min_count=None, enum_values=None, class_id='http://example.org/test-class', datatype='', pattern=''), Property(path='http://example.org/test-class/class-prop-no-class', varname='class-prop-no-class', comment='A test-class property with no sh:class', max_count=1, min_count=None, enum_values=None, class_id='http://example.org/test-class', datatype='', pattern=''), Property(path='http://example.org/test-class/datetime-list-prop', varname='datetime-list-prop', comment='A datetime list property', max_count=None, min_count=None, enum_values=None, class_id='', datatype='http://www.w3.org/2001/XMLSchema#dateTime', pattern=''), Property(path='http://example.org/test-class/datetime-scalar-prop', varname='datetime-scalar-prop', comment='A scalar datetime property', max_count=1, min_count=None, enum_values=None, class_id='', datatype='http://www.w3.org/2001/XMLSchema#dateTime', pattern=''), Property(path='http://example.org/test-class/datetimestamp-scalar-prop', varname='datetimestamp-scalar-prop', comment='A scalar dateTimeStamp property', max_count=1, min_count=None, enum_values=None, class_id='', datatype='http://www.w3.org/2001/XMLSchema#dateTimeStamp', pattern=''), Property(path='http://example.org/test-class/enum-list-prop', varname='enum-list-prop', comment='A enum list property', max_count=None, min_count=None, enum_values=[rdflib.term.URIRef('http://example.org/enumType/bar'), rdflib.term.URIRef('http://example.org/enumType/foo'), rdflib.term.URIRef('http://example.org/enumType/nolabel'), rdflib.term.URIRef('http://example.org/enumType/non-named-individual')], class_id='http://example.org/enumType', datatype='', pattern=''), Property(path='http://example.org/test-class/enum-prop', varname='enum-prop', comment='A enum property', max_count=1, min_count=None, enum_values=[rdflib.term.URIRef('http://example.org/enumType/bar'), rdflib.term.URIRef('http://example.org/enumType/foo'), rdflib.term.URIRef('http://example.org/enumType/nolabel'), rdflib.term.URIRef('http://example.org/enumType/non-named-individual')], class_id='http://example.org/enumType', datatype='', pattern=''), Property(path='http://example.org/test-class/enum-prop-no-class', varname='enum-prop-no-class', comment='A enum property with no sh:class', max_count=1, min_count=None, enum_values=[rdflib.term.URIRef('http://example.org/enumType/bar'), rdflib.term.URIRef('http://example.org/enumType/foo'), rdflib.term.URIRef('http://example.org/enumType/nolabel'), rdflib.term.URIRef('http://example.org/enumType/non-named-individual')], class_id='http://example.org/enumType', datatype='', pattern=''), Property(path='http://example.org/test-class/float-prop', varname='float-prop', comment='a float property', max_count=1, min_count=None, enum_values=None, class_id='', datatype='http://www.w3.org/2001/XMLSchema#decimal', pattern=''), Property(path='http://example.org/test-class/integer-prop', varname='integer-prop', comment='a non-negative integer', max_count=1, min_count=None, enum_values=None, class_id='', datatype='http://www.w3.org/2001/XMLSchema#integer', pattern=''), Property(path='http://example.org/test-class/named-property', varname=rdflib.term.Literal('named_property'), comment='A named property', max_count=1, min_count=None, enum_values=None, class_id='', datatype='http://www.w3.org/2001/XMLSchema#string', pattern=''), Property(path='http://example.org/test-class/non-shape', varname='non-shape', comment='A class with no shape', max_count=1, min_count=None, enum_values=None, class_id='http://example.org/non-shape-class', datatype='', pattern=''), Property(path='http://example.org/test-class/nonnegative-integer-prop', varname='nonnegative-integer-prop', comment='a non-negative integer', max_count=1, min_count=None, enum_values=None, class_id='', datatype='http://www.w3.org/2001/XMLSchema#nonNegativeInteger', pattern=''), Property(path='http://example.org/test-class/positive-integer-prop', varname='positive-integer-prop', comment='A positive integer', max_count=1, min_count=None, enum_values=None, class_id='', datatype='http://www.w3.org/2001/XMLSchema#positiveInteger', pattern=''), Property(path='http://example.org/test-class/regex', varname='regex', comment='A regex validated string', max_count=1, min_count=None, enum_values=None, class_id='', datatype='http://www.w3.org/2001/XMLSchema#string', pattern='^foo\\d'), Property(path='http://example.org/test-class/regex-datetime', varname='regex-datetime', comment='A regex dateTime', max_count=1, min_count=None, enum_values=None, class_id='', datatype='http://www.w3.org/2001/XMLSchema#dateTime', pattern='^\\d\\d\\d\\d-\\d\\d-\\d\\dT\\d\\d:\\d\\d:\\d\\d\\+01:00$'), Property(path='http://example.org/test-class/regex-datetimestamp', varname='regex-datetimestamp', comment='A regex dateTimeStamp', max_count=1, min_count=None, enum_values=None, class_id='', datatype='http://www.w3.org/2001/XMLSchema#dateTimeStamp', pattern='^\\d\\d\\d\\d-\\d\\d-\\d\\dT\\d\\d:\\d\\d:\\d\\dZ$'), Property(path='http://example.org/test-class/regex-list', varname='regex-list', comment='A regex validated string list', max_count=None, min_count=None, enum_values=None, class_id='', datatype='http://www.w3.org/2001/XMLSchema#string', pattern='^foo\\d'), Property(path='http://example.org/test-class/string-list-no-datatype', varname='string-list-no-datatype', comment='A string list property with no sh:datatype', max_count=None, min_count=None, enum_values=None, class_id='', datatype='http://www.w3.org/2001/XMLSchema#string', pattern=''), Property(path='http://example.org/test-class/string-list-prop', varname='string-list-prop', comment='A string list property', max_count=None, min_count=None, enum_values=None, class_id='', datatype='http://www.w3.org/2001/XMLSchema#string', pattern=''), Property(path='http://example.org/test-class/string-scalar-prop', varname='string-scalar-prop', comment='A scalar string propery', max_count=1, min_count=None, enum_values=None, class_id='', datatype='http://www.w3.org/2001/XMLSchema#string', pattern='')], comment='The test class', id_property=None, node_kind=rdflib.term.URIRef('http://www.w3.org/ns/shacl#BlankNodeOrIRI'), is_extensible=False, is_abstract=False, named_individuals=[Individual(_id='http://example.org/test-class/named', varname='named', comment='')]) Class(_id='http://example.org/test-class-required', clsname=['http', '//example.org/test-class-required'], parent_ids=['http://example.org/test-class'], derived_ids=[], properties=[Property(path='http://example.org/test-class/required-string-list-prop', varname='required-string-list-prop', comment='A required string list property', max_count=2, min_count=1, enum_values=None, class_id='', datatype='http://www.w3.org/2001/XMLSchema#string', pattern=''), Property(path='http://example.org/test-class/required-string-scalar-prop', varname='required-string-scalar-prop', comment='A required scalar string property', max_count=1, min_count=1, enum_values=None, class_id='', datatype='http://www.w3.org/2001/XMLSchema#string', pattern='')], comment='', id_property=None, node_kind=rdflib.term.URIRef('http://www.w3.org/ns/shacl#BlankNodeOrIRI'), is_extensible=False, is_abstract=False, named_individuals=[]) Class(_id='http://example.org/test-derived-class', clsname=['http', '//example.org/test-derived-class'], parent_ids=['http://example.org/test-class'], derived_ids=[], properties=[Property(path='http://example.org/test-derived-class/string-prop', varname='string-prop', comment='A string property in a derived class', max_count=1, min_count=None, enum_values=None, class_id='', datatype='http://www.w3.org/2001/XMLSchema#string', pattern='')], comment='A class derived from test-class', id_property=None, node_kind=rdflib.term.URIRef('http://www.w3.org/ns/shacl#BlankNodeOrIRI'), is_extensible=False, is_abstract=False, named_individuals=[]) Class(_id='http://example.org/aaa-derived-class', clsname=['http', '//example.org/aaa-derived-class'], parent_ids=['http://example.org/parent-class'], derived_ids=[], properties=[], comment='Derived class that sorts before the parent to test ordering', id_property=None, node_kind=rdflib.term.URIRef('http://www.w3.org/ns/shacl#BlankNodeOrIRI'), is_extensible=False, is_abstract=False, named_individuals=[]) diff --git a/tests/test_jsonschema.py b/tests/test_jsonschema.py index 44d4e666..18bf7ce6 100644 --- a/tests/test_jsonschema.py +++ b/tests/test_jsonschema.py @@ -815,10 +815,14 @@ def lst(v): {"@type": "test-derived-class"}, "_:blanknode", "http://serialize.example.org/test", + # Named individual + "test-class/named", ], bad=[ {"@type": "test-another-class"}, {"@type": "parent-class"}, + "not/an/iri", + "not-an-iri", ], typ=[object, str], ), From ff0712e3f62e80280077920bbb2dd95263871f48 Mon Sep 17 00:00:00 2001 From: Joshua Watt Date: Fri, 14 Jun 2024 16:20:45 -0600 Subject: [PATCH 07/14] python: Add support for shortened named individuals If a named individual is shortened by the context it need to be correctly mapped when encoding and decoding --- src/shacl2code/lang/templates/python.j2 | 33 +++++++++++++++++++++++++ tests/data/python/roundtrip.json | 5 ++++ tests/expect/python/test-context.py | 27 ++++++++++++++++++++ tests/expect/python/test.py | 19 ++++++++++++++ tests/test_python.py | 32 ++++++++++++++++++++++++ 5 files changed, 116 insertions(+) diff --git a/src/shacl2code/lang/templates/python.j2 b/src/shacl2code/lang/templates/python.j2 index a86101a9..56bf2214 100644 --- a/src/shacl2code/lang/templates/python.j2 +++ b/src/shacl2code/lang/templates/python.j2 @@ -264,6 +264,7 @@ class ObjectProp(Property): raise ValueError("Object cannot be None") if isinstance(value, str): + value = _NI_ENCODE_CONTEXT.get(value, value) encoder.write_iri(value) return @@ -274,6 +275,8 @@ class ObjectProp(Property): if iri is None: return self.cls.decode(decoder, objectset=objectset) + iri = _NI_DECODE_CONTEXT.get(iri, iri) + if objectset is None: return iri @@ -506,6 +509,8 @@ def register(type_iri, *, compact_type=None, abstract=False): SHACLObject.CLASSES[key] = c def decorator(c): + global NAMED_INDIVIDUALS + assert issubclass( c, SHACLObject ), f"{c.__name__} is not derived from SHACLObject" @@ -518,6 +523,8 @@ def register(type_iri, *, compact_type=None, abstract=False): if compact_type: add_class(compact_type, c) + NAMED_INDIVIDUALS |= set(c.NAMED_INDIVIDUALS.values()) + # Registration is deferred until the first instance of class is created # so that it has access to any other defined class c._NEEDS_REG = True @@ -527,6 +534,7 @@ def register(type_iri, *, compact_type=None, abstract=False): register_lock = threading.Lock() +NAMED_INDIVIDUALS = set() @functools.total_ordering @@ -1079,6 +1087,8 @@ class SHACLObjectSet(object): return self._link() def _link(self): + global NAMED_INDIVIDUALS + self.missing_ids = set() visited = set() @@ -1101,6 +1111,9 @@ class SHACLObjectSet(object): obj_by_id[_id] = obj self.obj_by_id = obj_by_id + # Named individuals aren't considered missing + self.missing_ids -= NAMED_INDIVIDUALS + return self.missing_ids def find_by_id(self, _id, default=None): @@ -1938,6 +1951,26 @@ CONTEXT_URLS = [ {%- endfor %} ] +_NI_ENCODE_CONTEXT = { +{%- for class in classes %} +{%- for member in class.named_individuals %} + {%- if context.compact(member._id) != member._id %} + "{{ member._id }}": "{{ context.compact(member._id) }}", + {%- endif %} +{%- endfor %} +{%- endfor %} +} + +_NI_DECODE_CONTEXT = { +{%- for class in classes %} +{%- for member in class.named_individuals %} + {%- if context.compact(member._id) != member._id %} + "{{ context.compact(member._id) }}": "{{ member._id }}", + {%- endif %} +{%- endfor %} +{%- endfor %} +} + # CLASSES {%- for class in classes %} diff --git a/tests/data/python/roundtrip.json b/tests/data/python/roundtrip.json index 4888908c..336c7274 100644 --- a/tests/data/python/roundtrip.json +++ b/tests/data/python/roundtrip.json @@ -76,6 +76,11 @@ "@id": "http://serialize.example.com/test-derived", "@type": "test-derived-class" }, + { + "@type": "test-class", + "@id": "http://serialize.example.com/test-named-individual-reference", + "test-class/class-prop": "test-class/named" + }, { "@type": "test-class", "@id": "http://serialize.example.com/test-special-chars", diff --git a/tests/expect/python/test-context.py b/tests/expect/python/test-context.py index 0ab47d3b..b64d3813 100755 --- a/tests/expect/python/test-context.py +++ b/tests/expect/python/test-context.py @@ -264,6 +264,7 @@ def encode(self, encoder, value, state): raise ValueError("Object cannot be None") if isinstance(value, str): + value = _NI_ENCODE_CONTEXT.get(value, value) encoder.write_iri(value) return @@ -274,6 +275,8 @@ def decode(self, decoder, *, objectset=None): if iri is None: return self.cls.decode(decoder, objectset=objectset) + iri = _NI_DECODE_CONTEXT.get(iri, iri) + if objectset is None: return iri @@ -506,6 +509,8 @@ def add_class(key, c): SHACLObject.CLASSES[key] = c def decorator(c): + global NAMED_INDIVIDUALS + assert issubclass( c, SHACLObject ), f"{c.__name__} is not derived from SHACLObject" @@ -518,6 +523,8 @@ def decorator(c): if compact_type: add_class(compact_type, c) + NAMED_INDIVIDUALS |= set(c.NAMED_INDIVIDUALS.values()) + # Registration is deferred until the first instance of class is created # so that it has access to any other defined class c._NEEDS_REG = True @@ -527,6 +534,7 @@ def decorator(c): register_lock = threading.Lock() +NAMED_INDIVIDUALS = set() @functools.total_ordering @@ -1079,6 +1087,8 @@ def link(self): return self._link() def _link(self): + global NAMED_INDIVIDUALS + self.missing_ids = set() visited = set() @@ -1101,6 +1111,9 @@ def _link(self): obj_by_id[_id] = obj self.obj_by_id = obj_by_id + # Named individuals aren't considered missing + self.missing_ids -= NAMED_INDIVIDUALS + return self.missing_ids def find_by_id(self, _id, default=None): @@ -1924,6 +1937,20 @@ def callback(value, path): "https://spdx.github.io/spdx-3-model/context.json", ] +_NI_ENCODE_CONTEXT = { + "http://example.org/enumType/foo": "enumType/foo", + "http://example.org/enumType/bar": "enumType/bar", + "http://example.org/enumType/nolabel": "enumType/nolabel", + "http://example.org/test-class/named": "test-class/named", +} + +_NI_DECODE_CONTEXT = { + "enumType/foo": "http://example.org/enumType/foo", + "enumType/bar": "http://example.org/enumType/bar", + "enumType/nolabel": "http://example.org/enumType/nolabel", + "test-class/named": "http://example.org/test-class/named", +} + # CLASSES # An Abstract class diff --git a/tests/expect/python/test.py b/tests/expect/python/test.py index 701d21fc..4d3f7c5f 100644 --- a/tests/expect/python/test.py +++ b/tests/expect/python/test.py @@ -264,6 +264,7 @@ def encode(self, encoder, value, state): raise ValueError("Object cannot be None") if isinstance(value, str): + value = _NI_ENCODE_CONTEXT.get(value, value) encoder.write_iri(value) return @@ -274,6 +275,8 @@ def decode(self, decoder, *, objectset=None): if iri is None: return self.cls.decode(decoder, objectset=objectset) + iri = _NI_DECODE_CONTEXT.get(iri, iri) + if objectset is None: return iri @@ -506,6 +509,8 @@ def add_class(key, c): SHACLObject.CLASSES[key] = c def decorator(c): + global NAMED_INDIVIDUALS + assert issubclass( c, SHACLObject ), f"{c.__name__} is not derived from SHACLObject" @@ -518,6 +523,8 @@ def decorator(c): if compact_type: add_class(compact_type, c) + NAMED_INDIVIDUALS |= set(c.NAMED_INDIVIDUALS.values()) + # Registration is deferred until the first instance of class is created # so that it has access to any other defined class c._NEEDS_REG = True @@ -527,6 +534,7 @@ def decorator(c): register_lock = threading.Lock() +NAMED_INDIVIDUALS = set() @functools.total_ordering @@ -1079,6 +1087,8 @@ def link(self): return self._link() def _link(self): + global NAMED_INDIVIDUALS + self.missing_ids = set() visited = set() @@ -1101,6 +1111,9 @@ def _link(self): obj_by_id[_id] = obj self.obj_by_id = obj_by_id + # Named individuals aren't considered missing + self.missing_ids -= NAMED_INDIVIDUALS + return self.missing_ids def find_by_id(self, _id, default=None): @@ -1923,6 +1936,12 @@ def callback(value, path): CONTEXT_URLS = [ ] +_NI_ENCODE_CONTEXT = { +} + +_NI_DECODE_CONTEXT = { +} + # CLASSES # An Abstract class diff --git a/tests/test_python.py b/tests/test_python.py index c66ea001..22b70392 100644 --- a/tests/test_python.py +++ b/tests/test_python.py @@ -754,6 +754,7 @@ def type_tests(name, *typ): ), ("test_class_class_prop", lambda model: model.test_another_class(), TypeError), ("test_class_class_prop", lambda model: model.parent_class(), TypeError), + ("test_class_class_prop", lambda model: model.test_class.named, SAME_AS_VALUE), ("test_class_class_prop", "_:blanknode", "_:blanknode"), ( "test_class_class_prop", @@ -1619,6 +1620,11 @@ class Extension(model.extensible_class): expect.add(objset.find_by_id("http://serialize.example.com/test")) expect.add(objset.find_by_id("http://serialize.example.com/nested-parent")) + expect.add( + objset.find_by_id( + "http://serialize.example.com/test-named-individual-reference" + ) + ) expect.add( objset.find_by_id( "http://serialize.example.com/nested-parent" @@ -1739,3 +1745,29 @@ def test_required_abstract_class_property(model, tmp_path): # Deleting an abstract class property should return it to None del c.required_abstract_abstract_class_prop assert c.required_abstract_abstract_class_prop is None + + +def test_named_individual(model, roundtrip): + objset = model.SHACLObjectSet() + with roundtrip.open("r") as f: + d = model.JSONLDDeserializer() + d.read(f, objset) + + c = objset.find_by_id( + "http://serialize.example.com/test-named-individual-reference" + ) + assert c is not None + assert c.test_class_class_prop == model.test_class.named + + assert model.test_class.named not in objset.missing_ids + + +def test_missing_ids(model, roundtrip): + objset = model.SHACLObjectSet() + with roundtrip.open("r") as f: + d = model.JSONLDDeserializer() + d.read(f, objset) + + assert objset.missing_ids == { + "http://serialize.example.com/non-shape", + } From dc1772ec833110ac079cb3b7653e8d324c77cee6 Mon Sep 17 00:00:00 2001 From: Joshua Watt Date: Tue, 18 Jun 2024 15:49:43 -0600 Subject: [PATCH 08/14] Correctly handle extensible abstract classes Extensible abstract classes are a little strange, but it basically means that the type of the class can be anything _except_ the specific extensible parent type. Implement this in python and jsonschema --- src/shacl2code/lang/templates/jsonschema.j2 | 10 ++- src/shacl2code/lang/templates/python.j2 | 16 +++- tests/data/model/test-context.json | 4 + tests/data/model/test.ttl | 21 +++++ tests/data/python/roundtrip.json | 7 ++ tests/expect/jsonschema/test-context.json | 88 +++++++++++++++++++++ tests/expect/jsonschema/test.json | 88 +++++++++++++++++++++ tests/expect/python/test-context.py | 44 ++++++++++- tests/expect/python/test.py | 43 +++++++++- tests/expect/raw/test-context.txt | 4 + tests/expect/raw/test.txt | 2 + tests/test_jsonschema.py | 32 ++++++++ tests/test_python.py | 14 ++++ 13 files changed, 366 insertions(+), 7 deletions(-) diff --git a/src/shacl2code/lang/templates/jsonschema.j2 b/src/shacl2code/lang/templates/jsonschema.j2 index 91cda97a..ecce794e 100644 --- a/src/shacl2code/lang/templates/jsonschema.j2 +++ b/src/shacl2code/lang/templates/jsonschema.j2 @@ -59,7 +59,7 @@ {#- Classes are divided into 2 parts. The properties are separated into a separate object #} {#- so that a object can references the properties of its parent without needing the const #} {#- @type tag #} - {%- if not class.is_abstract %} + {%- if not class.is_abstract or class.is_extensible %} "{{ varname(*class.clsname) }}": { "allOf": [ { @@ -70,12 +70,20 @@ "properties": { "{{ class.id_property or "@id" }}": { "$ref": "#/$defs/{{ class.node_kind.split("#")[-1] }}" }, "{{ context.compact("@type") }}": { + {#- Abstract Extensible classes are weird; any type _except_ the specific class type is allowed #} + {%- if class.is_abstract and class.is_extensible %} + "allOf": [ + { "$ref": "#/$defs/IRI" }, + { "not": { "const": "{{ context.compact_vocab(class._id) }}" } } + ] + {%- else %} "oneOf": [ {%- if class.is_extensible %} { "$ref": "#/$defs/IRI" }, {%- endif %} { "const": "{{ context.compact_vocab(class._id) }}" } ] + {%- endif %} } }{%- if class.node_kind == SH.IRI %}, "required": ["{{ class.id_property or "@id" }}"] diff --git a/src/shacl2code/lang/templates/python.j2 b/src/shacl2code/lang/templates/python.j2 index 56bf2214..4ea8f7b6 100644 --- a/src/shacl2code/lang/templates/python.j2 +++ b/src/shacl2code/lang/templates/python.j2 @@ -545,7 +545,7 @@ class SHACLObject(object): IS_ABSTRACT = True def __init__(self, **kwargs): - if self.__class__.IS_ABSTRACT: + if self._is_abstract(): raise NotImplementedError( f"{self.__class__.__name__} is abstract and cannot be implemented" ) @@ -567,6 +567,9 @@ class SHACLObject(object): for k, v in kwargs.items(): setattr(self, k, v) + def _is_abstract(self): + return self.__class__.IS_ABSTRACT + @classmethod def _register_props(cls): cls._add_property("_id", StringProp(), iri="@id") @@ -883,11 +886,20 @@ class SHACLExtensibleObject(object): CLOSED = False def __init__(self, typ=None, **kwargs): - super().__init__(**kwargs) if typ: self.__dict__["_obj_TYPE"] = (typ, None) else: self.__dict__["_obj_TYPE"] = (self._OBJ_TYPE, self._OBJ_COMPACT_TYPE) + super().__init__(**kwargs) + + def _is_abstract(self): + # Unknown classes are assumed to not be abstract so that they can be + # deserialized + typ = self.__dict__["_obj_TYPE"][0] + if typ in self.__class__.CLASSES: + return self.__class__.CLASSES[typ].IS_ABSTRACT + + return False @classmethod def _make_object(cls, typ): diff --git a/tests/data/model/test-context.json b/tests/data/model/test-context.json index abf3692f..a01ee20d 100644 --- a/tests/data/model/test-context.json +++ b/tests/data/model/test-context.json @@ -131,6 +131,10 @@ "extensible-class/required": { "@id": "test:extensible-class/required", "@type": "xsd:string" + }, + "uses-extensible-abstract-class/prop": { + "@id": "test:uses-extensible-abstract-class/prop", + "@type": "@id" } } } diff --git a/tests/data/model/test.ttl b/tests/data/model/test.ttl index f6dc4e3c..bcfd4605 100644 --- a/tests/data/model/test.ttl +++ b/tests/data/model/test.ttl @@ -514,3 +514,24 @@ rdfs:comment "A required abstract class property" ; rdfs:range . + + a rdf:Class, sh:NodeShape, owl:Class ; + sh-to-code:isExtensible true ; + sh-to-code:isAbstract true ; + rdfs:comment "An extensible abstract class" + . + + a rdf:Class, sh:NodeShape, owl:Class ; + rdfs:comment "A class that uses an abstract extensible class" ; + sh:property [ + sh:class ; + sh:path ; + sh:minCount 1 ; + sh:maxCount 1 + ] + . + + a rdf:Property ; + rdfs:comment "A property that references and abstract extensible class" ; + rdfs:range + . diff --git a/tests/data/python/roundtrip.json b/tests/data/python/roundtrip.json index 336c7274..30e16ad6 100644 --- a/tests/data/python/roundtrip.json +++ b/tests/data/python/roundtrip.json @@ -85,6 +85,13 @@ "@type": "test-class", "@id": "http://serialize.example.com/test-special-chars", "test-class/string-scalar-prop": "special chars \"\n\r:{}[]" + }, + { + "@type": "uses-extensible-abstract-class", + "@id": "http://serialize.example.com/test-uses-extensible-abstract", + "uses-extensible-abstract-class/prop": { + "@type": " http://serialize.example.com/custom-extensible" + } } ] } diff --git a/tests/expect/jsonschema/test-context.json b/tests/expect/jsonschema/test-context.json index 73b62e9d..a7f4176d 100644 --- a/tests/expect/jsonschema/test-context.json +++ b/tests/expect/jsonschema/test-context.json @@ -194,6 +194,45 @@ } ] }, + "extensibleabstractclass": { + "allOf": [ + { + "type": "object", + "unevaluatedProperties": true, + "properties": { + "@id": { "$ref": "#/$defs/BlankNodeOrIRI" }, + "@type": { + "allOf": [ + { "$ref": "#/$defs/IRI" }, + { "not": { "const": "extensible-abstract-class" } } + ] + } + } + }, + { "$ref": "#/$defs/extensibleabstractclass_props" } + ] + }, + "extensibleabstractclass_derived": { + "anyOf": [ + { + "type": "object", + "anyOf": [ + { "$ref": "#/$defs/extensibleabstractclass" } + ] + }, + { "$ref": "#/$defs/BlankNodeOrIRI" } + ] + }, + "extensibleabstractclass_props": { + "allOf": [ + { "$ref": "#/$defs/SHACLClass" }, + { + "type": "object", + "properties": { + } + } + ] + }, "idpropclass": { "allOf": [ { @@ -1070,6 +1109,53 @@ "prop_testderivedclass_testderivedclassstringprop": { "type": "string" }, + "usesextensibleabstractclass": { + "allOf": [ + { + "type": "object", + "properties": { + "@id": { "$ref": "#/$defs/BlankNodeOrIRI" }, + "@type": { + "oneOf": [ + { "const": "uses-extensible-abstract-class" } + ] + } + } + }, + { "$ref": "#/$defs/usesextensibleabstractclass_props" } + ] + }, + "usesextensibleabstractclass_derived": { + "anyOf": [ + { + "type": "object", + "unevaluatedProperties": false, + "anyOf": [ + { "$ref": "#/$defs/usesextensibleabstractclass" } + ] + }, + { "$ref": "#/$defs/BlankNodeOrIRI" } + ] + }, + "usesextensibleabstractclass_props": { + "allOf": [ + { "$ref": "#/$defs/SHACLClass" }, + { + "type": "object", + "properties": { + "uses-extensible-abstract-class/prop": { + "$ref": "#/$defs/prop_usesextensibleabstractclass_usesextensibleabstractclassprop" + } + }, + "required": [ + "uses-extensible-abstract-class/prop" + ] + } + ] + }, + "prop_usesextensibleabstractclass_usesextensibleabstractclassprop": { + "$ref": "#/$defs/extensibleabstractclass_derived" + }, "aaaderivedclass": { "allOf": [ { @@ -1243,6 +1329,7 @@ "test-class", "test-class-required", "test-derived-class", + "uses-extensible-abstract-class", "aaa-derived-class", "derived-node-kind-iri", "extensible-class" @@ -1272,6 +1359,7 @@ { "$ref": "#/$defs/testclass" }, { "$ref": "#/$defs/testclassrequired" }, { "$ref": "#/$defs/testderivedclass" }, + { "$ref": "#/$defs/usesextensibleabstractclass" }, { "$ref": "#/$defs/aaaderivedclass" }, { "$ref": "#/$defs/derivednodekindiri" }, { "$ref": "#/$defs/extensibleclass" } diff --git a/tests/expect/jsonschema/test.json b/tests/expect/jsonschema/test.json index c433b123..d113e5d5 100644 --- a/tests/expect/jsonschema/test.json +++ b/tests/expect/jsonschema/test.json @@ -187,6 +187,45 @@ } ] }, + "http_exampleorgextensibleabstractclass": { + "allOf": [ + { + "type": "object", + "unevaluatedProperties": true, + "properties": { + "@id": { "$ref": "#/$defs/BlankNodeOrIRI" }, + "@type": { + "allOf": [ + { "$ref": "#/$defs/IRI" }, + { "not": { "const": "http://example.org/extensible-abstract-class" } } + ] + } + } + }, + { "$ref": "#/$defs/http_exampleorgextensibleabstractclass_props" } + ] + }, + "http_exampleorgextensibleabstractclass_derived": { + "anyOf": [ + { + "type": "object", + "anyOf": [ + { "$ref": "#/$defs/http_exampleorgextensibleabstractclass" } + ] + }, + { "$ref": "#/$defs/BlankNodeOrIRI" } + ] + }, + "http_exampleorgextensibleabstractclass_props": { + "allOf": [ + { "$ref": "#/$defs/SHACLClass" }, + { + "type": "object", + "properties": { + } + } + ] + }, "http_exampleorgidpropclass": { "allOf": [ { @@ -1061,6 +1100,53 @@ "prop_http_exampleorgtestderivedclass_stringprop": { "type": "string" }, + "http_exampleorgusesextensibleabstractclass": { + "allOf": [ + { + "type": "object", + "properties": { + "@id": { "$ref": "#/$defs/BlankNodeOrIRI" }, + "@type": { + "oneOf": [ + { "const": "http://example.org/uses-extensible-abstract-class" } + ] + } + } + }, + { "$ref": "#/$defs/http_exampleorgusesextensibleabstractclass_props" } + ] + }, + "http_exampleorgusesextensibleabstractclass_derived": { + "anyOf": [ + { + "type": "object", + "unevaluatedProperties": false, + "anyOf": [ + { "$ref": "#/$defs/http_exampleorgusesextensibleabstractclass" } + ] + }, + { "$ref": "#/$defs/BlankNodeOrIRI" } + ] + }, + "http_exampleorgusesextensibleabstractclass_props": { + "allOf": [ + { "$ref": "#/$defs/SHACLClass" }, + { + "type": "object", + "properties": { + "http://example.org/uses-extensible-abstract-class/prop": { + "$ref": "#/$defs/prop_http_exampleorgusesextensibleabstractclass_prop" + } + }, + "required": [ + "http://example.org/uses-extensible-abstract-class/prop" + ] + } + ] + }, + "prop_http_exampleorgusesextensibleabstractclass_prop": { + "$ref": "#/$defs/http_exampleorgextensibleabstractclass_derived" + }, "http_exampleorgaaaderivedclass": { "allOf": [ { @@ -1234,6 +1320,7 @@ "http://example.org/test-class", "http://example.org/test-class-required", "http://example.org/test-derived-class", + "http://example.org/uses-extensible-abstract-class", "http://example.org/aaa-derived-class", "http://example.org/derived-node-kind-iri", "http://example.org/extensible-class" @@ -1263,6 +1350,7 @@ { "$ref": "#/$defs/http_exampleorgtestclass" }, { "$ref": "#/$defs/http_exampleorgtestclassrequired" }, { "$ref": "#/$defs/http_exampleorgtestderivedclass" }, + { "$ref": "#/$defs/http_exampleorgusesextensibleabstractclass" }, { "$ref": "#/$defs/http_exampleorgaaaderivedclass" }, { "$ref": "#/$defs/http_exampleorgderivednodekindiri" }, { "$ref": "#/$defs/http_exampleorgextensibleclass" } diff --git a/tests/expect/python/test-context.py b/tests/expect/python/test-context.py index b64d3813..4a3d44d9 100755 --- a/tests/expect/python/test-context.py +++ b/tests/expect/python/test-context.py @@ -545,7 +545,7 @@ class SHACLObject(object): IS_ABSTRACT = True def __init__(self, **kwargs): - if self.__class__.IS_ABSTRACT: + if self._is_abstract(): raise NotImplementedError( f"{self.__class__.__name__} is abstract and cannot be implemented" ) @@ -567,6 +567,9 @@ def __init__(self, **kwargs): for k, v in kwargs.items(): setattr(self, k, v) + def _is_abstract(self): + return self.__class__.IS_ABSTRACT + @classmethod def _register_props(cls): cls._add_property("_id", StringProp(), iri="@id") @@ -883,11 +886,20 @@ class SHACLExtensibleObject(object): CLOSED = False def __init__(self, typ=None, **kwargs): - super().__init__(**kwargs) if typ: self.__dict__["_obj_TYPE"] = (typ, None) else: self.__dict__["_obj_TYPE"] = (self._OBJ_TYPE, self._OBJ_COMPACT_TYPE) + super().__init__(**kwargs) + + def _is_abstract(self): + # Unknown classes are assumed to not be abstract so that they can be + # deserialized + typ = self.__dict__["_obj_TYPE"][0] + if typ in self.__class__.CLASSES: + return self.__class__.CLASSES[typ].IS_ABSTRACT + + return False @classmethod def _make_object(cls, typ): @@ -2002,6 +2014,14 @@ class enumType(SHACLObject): nolabel = "http://example.org/enumType/nolabel" +# An extensible abstract class +@register("http://example.org/extensible-abstract-class", compact_type="extensible-abstract-class", abstract=True) +class extensible_abstract_class(SHACLExtensibleObject, SHACLObject): + NODE_KIND = NodeKind.BlankNodeOrIRI + NAMED_INDIVIDUALS = { + } + + # A class with an ID alias @register("http://example.org/id-prop-class", compact_type="id-prop-class", abstract=False) class id_prop_class(SHACLObject): @@ -2394,6 +2414,26 @@ def _register_props(cls): ) +# A class that uses an abstract extensible class +@register("http://example.org/uses-extensible-abstract-class", compact_type="uses-extensible-abstract-class", abstract=False) +class uses_extensible_abstract_class(SHACLObject): + NODE_KIND = NodeKind.BlankNodeOrIRI + NAMED_INDIVIDUALS = { + } + + @classmethod + def _register_props(cls): + super()._register_props() + # A property that references and abstract extensible class + cls._add_property( + "uses_extensible_abstract_class_prop", + ObjectProp(extensible_abstract_class, True), + iri="http://example.org/uses-extensible-abstract-class/prop", + min_count=1, + compact="uses-extensible-abstract-class/prop", + ) + + # Derived class that sorts before the parent to test ordering @register("http://example.org/aaa-derived-class", compact_type="aaa-derived-class", abstract=False) class aaa_derived_class(parent_class): diff --git a/tests/expect/python/test.py b/tests/expect/python/test.py index 4d3f7c5f..cd689f7b 100644 --- a/tests/expect/python/test.py +++ b/tests/expect/python/test.py @@ -545,7 +545,7 @@ class SHACLObject(object): IS_ABSTRACT = True def __init__(self, **kwargs): - if self.__class__.IS_ABSTRACT: + if self._is_abstract(): raise NotImplementedError( f"{self.__class__.__name__} is abstract and cannot be implemented" ) @@ -567,6 +567,9 @@ def __init__(self, **kwargs): for k, v in kwargs.items(): setattr(self, k, v) + def _is_abstract(self): + return self.__class__.IS_ABSTRACT + @classmethod def _register_props(cls): cls._add_property("_id", StringProp(), iri="@id") @@ -883,11 +886,20 @@ class SHACLExtensibleObject(object): CLOSED = False def __init__(self, typ=None, **kwargs): - super().__init__(**kwargs) if typ: self.__dict__["_obj_TYPE"] = (typ, None) else: self.__dict__["_obj_TYPE"] = (self._OBJ_TYPE, self._OBJ_COMPACT_TYPE) + super().__init__(**kwargs) + + def _is_abstract(self): + # Unknown classes are assumed to not be abstract so that they can be + # deserialized + typ = self.__dict__["_obj_TYPE"][0] + if typ in self.__class__.CLASSES: + return self.__class__.CLASSES[typ].IS_ABSTRACT + + return False @classmethod def _make_object(cls, typ): @@ -1993,6 +2005,14 @@ class http_example_org_enumType(SHACLObject): nolabel = "http://example.org/enumType/nolabel" +# An extensible abstract class +@register("http://example.org/extensible-abstract-class", abstract=True) +class http_example_org_extensible_abstract_class(SHACLExtensibleObject, SHACLObject): + NODE_KIND = NodeKind.BlankNodeOrIRI + NAMED_INDIVIDUALS = { + } + + # A class with an ID alias @register("http://example.org/id-prop-class", abstract=False) class http_example_org_id_prop_class(SHACLObject): @@ -2351,6 +2371,25 @@ def _register_props(cls): ) +# A class that uses an abstract extensible class +@register("http://example.org/uses-extensible-abstract-class", abstract=False) +class http_example_org_uses_extensible_abstract_class(SHACLObject): + NODE_KIND = NodeKind.BlankNodeOrIRI + NAMED_INDIVIDUALS = { + } + + @classmethod + def _register_props(cls): + super()._register_props() + # A property that references and abstract extensible class + cls._add_property( + "prop", + ObjectProp(http_example_org_extensible_abstract_class, True), + iri="http://example.org/uses-extensible-abstract-class/prop", + min_count=1, + ) + + # Derived class that sorts before the parent to test ordering @register("http://example.org/aaa-derived-class", abstract=False) class http_example_org_aaa_derived_class(http_example_org_parent_class): diff --git a/tests/expect/raw/test-context.txt b/tests/expect/raw/test-context.txt index 156fc912..752b91fd 100644 --- a/tests/expect/raw/test-context.txt +++ b/tests/expect/raw/test-context.txt @@ -11,6 +11,8 @@ http://example.org/concrete-spdx-class: concrete-spdx-class http://example.org/enumType: enumType +http://example.org/extensible-abstract-class: extensible-abstract-class + http://example.org/id-prop-class: id-prop-class http://example.org/inherited-id-prop-class: inherited-id-prop-class @@ -39,6 +41,8 @@ http://example.org/test-class-required: test-class-required http://example.org/test-derived-class: test-derived-class +http://example.org/uses-extensible-abstract-class: uses-extensible-abstract-class + http://example.org/aaa-derived-class: aaa-derived-class http://example.org/derived-node-kind-iri: derived-node-kind-iri diff --git a/tests/expect/raw/test.txt b/tests/expect/raw/test.txt index 70c83b29..d248cab8 100644 --- a/tests/expect/raw/test.txt +++ b/tests/expect/raw/test.txt @@ -5,6 +5,7 @@ Class(_id='http://example.org/abstract-spdx-class', clsname=['http', '//example. Class(_id='http://example.org/concrete-class', clsname=['http', '//example.org/concrete-class'], parent_ids=['http://example.org/abstract-class'], derived_ids=[], properties=[], comment='A concrete class', id_property=None, node_kind=rdflib.term.URIRef('http://www.w3.org/ns/shacl#BlankNodeOrIRI'), is_extensible=False, is_abstract=False, named_individuals=[]) Class(_id='http://example.org/concrete-spdx-class', clsname=['http', '//example.org/concrete-spdx-class'], parent_ids=['http://example.org/abstract-spdx-class'], derived_ids=[], properties=[], comment='A concrete class', id_property=None, node_kind=rdflib.term.URIRef('http://www.w3.org/ns/shacl#BlankNodeOrIRI'), is_extensible=False, is_abstract=False, named_individuals=[]) Class(_id='http://example.org/enumType', clsname=['http', '//example.org/enumType'], parent_ids=[], derived_ids=[], properties=[], comment='An enumerated type', id_property=None, node_kind=rdflib.term.URIRef('http://www.w3.org/ns/shacl#BlankNodeOrIRI'), is_extensible=False, is_abstract=False, named_individuals=[Individual(_id='http://example.org/enumType/foo', varname='foo', comment='The foo value of enumType'), Individual(_id='http://example.org/enumType/bar', varname='bar', comment='The bar value of enumType'), Individual(_id='http://example.org/enumType/nolabel', varname='nolabel', comment='This value has no label')]) +Class(_id='http://example.org/extensible-abstract-class', clsname=['http', '//example.org/extensible-abstract-class'], parent_ids=[], derived_ids=[], properties=[], comment='An extensible abstract class', id_property=None, node_kind=rdflib.term.URIRef('http://www.w3.org/ns/shacl#BlankNodeOrIRI'), is_extensible=True, is_abstract=True, named_individuals=[]) Class(_id='http://example.org/id-prop-class', clsname=['http', '//example.org/id-prop-class'], parent_ids=[], derived_ids=['http://example.org/inherited-id-prop-class'], properties=[], comment='A class with an ID alias', id_property='testid', node_kind=rdflib.term.URIRef('http://www.w3.org/ns/shacl#BlankNodeOrIRI'), is_extensible=False, is_abstract=False, named_individuals=[]) Class(_id='http://example.org/inherited-id-prop-class', clsname=['http', '//example.org/inherited-id-prop-class'], parent_ids=['http://example.org/id-prop-class'], derived_ids=[], properties=[], comment='A class that inherits its idPropertyName from the parent', id_property='testid', node_kind=rdflib.term.URIRef('http://www.w3.org/ns/shacl#BlankNodeOrIRI'), is_extensible=False, is_abstract=False, named_individuals=[]) Class(_id='http://example.org/link-class', clsname=['http', '//example.org/link-class'], parent_ids=[], derived_ids=['http://example.org/extensible-class', 'http://example.org/link-derived-class', 'http://example.org/node-kind-blank', 'http://example.org/node-kind-iri', 'http://example.org/node-kind-iri-or-blank'], properties=[Property(path='http://example.org/link-class-extensible', varname='-extensible', comment='A link to an extensible-class', max_count=1, min_count=None, enum_values=None, class_id='http://example.org/extensible-class', datatype='', pattern=''), Property(path='http://example.org/link-class-link-list-prop', varname='-link-list-prop', comment='A link-class list property', max_count=None, min_count=None, enum_values=None, class_id='http://example.org/link-class', datatype='', pattern=''), Property(path='http://example.org/link-class-link-prop', varname='-link-prop', comment='A link-class property', max_count=1, min_count=None, enum_values=None, class_id='http://example.org/link-class', datatype='', pattern=''), Property(path='http://example.org/link-class-link-prop-no-class', varname='-link-prop-no-class', comment='A link-class property with no sh:class', max_count=1, min_count=None, enum_values=None, class_id='http://example.org/link-class', datatype='', pattern='')], comment='A class to test links', id_property=None, node_kind=rdflib.term.URIRef('http://www.w3.org/ns/shacl#BlankNodeOrIRI'), is_extensible=False, is_abstract=False, named_individuals=[]) @@ -19,6 +20,7 @@ Class(_id='http://example.org/test-another-class', clsname=['http', '//example.o Class(_id='http://example.org/test-class', clsname=['http', '//example.org/test-class'], parent_ids=['http://example.org/parent-class'], derived_ids=['http://example.org/test-class-required', 'http://example.org/test-derived-class'], properties=[Property(path='http://example.org/encode', varname='encode', comment='A property that conflicts with an existing SHACLObject property', max_count=1, min_count=None, enum_values=None, class_id='', datatype='http://www.w3.org/2001/XMLSchema#string', pattern=''), Property(path='http://example.org/import', varname='import', comment='A property that is a keyword', max_count=1, min_count=None, enum_values=None, class_id='', datatype='http://www.w3.org/2001/XMLSchema#string', pattern=''), Property(path='http://example.org/test-class/anyuri-prop', varname='anyuri-prop', comment='a URI', max_count=1, min_count=None, enum_values=None, class_id='', datatype='http://www.w3.org/2001/XMLSchema#anyURI', pattern=''), Property(path='http://example.org/test-class/boolean-prop', varname='boolean-prop', comment='a boolean property', max_count=1, min_count=None, enum_values=None, class_id='', datatype='http://www.w3.org/2001/XMLSchema#boolean', pattern=''), Property(path='http://example.org/test-class/class-list-prop', varname='class-list-prop', comment='A test-class list property', max_count=None, min_count=None, enum_values=None, class_id='http://example.org/test-class', datatype='', pattern=''), Property(path='http://example.org/test-class/class-prop', varname='class-prop', comment='A test-class property', max_count=1, min_count=None, enum_values=None, class_id='http://example.org/test-class', datatype='', pattern=''), Property(path='http://example.org/test-class/class-prop-no-class', varname='class-prop-no-class', comment='A test-class property with no sh:class', max_count=1, min_count=None, enum_values=None, class_id='http://example.org/test-class', datatype='', pattern=''), Property(path='http://example.org/test-class/datetime-list-prop', varname='datetime-list-prop', comment='A datetime list property', max_count=None, min_count=None, enum_values=None, class_id='', datatype='http://www.w3.org/2001/XMLSchema#dateTime', pattern=''), Property(path='http://example.org/test-class/datetime-scalar-prop', varname='datetime-scalar-prop', comment='A scalar datetime property', max_count=1, min_count=None, enum_values=None, class_id='', datatype='http://www.w3.org/2001/XMLSchema#dateTime', pattern=''), Property(path='http://example.org/test-class/datetimestamp-scalar-prop', varname='datetimestamp-scalar-prop', comment='A scalar dateTimeStamp property', max_count=1, min_count=None, enum_values=None, class_id='', datatype='http://www.w3.org/2001/XMLSchema#dateTimeStamp', pattern=''), Property(path='http://example.org/test-class/enum-list-prop', varname='enum-list-prop', comment='A enum list property', max_count=None, min_count=None, enum_values=[rdflib.term.URIRef('http://example.org/enumType/bar'), rdflib.term.URIRef('http://example.org/enumType/foo'), rdflib.term.URIRef('http://example.org/enumType/nolabel'), rdflib.term.URIRef('http://example.org/enumType/non-named-individual')], class_id='http://example.org/enumType', datatype='', pattern=''), Property(path='http://example.org/test-class/enum-prop', varname='enum-prop', comment='A enum property', max_count=1, min_count=None, enum_values=[rdflib.term.URIRef('http://example.org/enumType/bar'), rdflib.term.URIRef('http://example.org/enumType/foo'), rdflib.term.URIRef('http://example.org/enumType/nolabel'), rdflib.term.URIRef('http://example.org/enumType/non-named-individual')], class_id='http://example.org/enumType', datatype='', pattern=''), Property(path='http://example.org/test-class/enum-prop-no-class', varname='enum-prop-no-class', comment='A enum property with no sh:class', max_count=1, min_count=None, enum_values=[rdflib.term.URIRef('http://example.org/enumType/bar'), rdflib.term.URIRef('http://example.org/enumType/foo'), rdflib.term.URIRef('http://example.org/enumType/nolabel'), rdflib.term.URIRef('http://example.org/enumType/non-named-individual')], class_id='http://example.org/enumType', datatype='', pattern=''), Property(path='http://example.org/test-class/float-prop', varname='float-prop', comment='a float property', max_count=1, min_count=None, enum_values=None, class_id='', datatype='http://www.w3.org/2001/XMLSchema#decimal', pattern=''), Property(path='http://example.org/test-class/integer-prop', varname='integer-prop', comment='a non-negative integer', max_count=1, min_count=None, enum_values=None, class_id='', datatype='http://www.w3.org/2001/XMLSchema#integer', pattern=''), Property(path='http://example.org/test-class/named-property', varname=rdflib.term.Literal('named_property'), comment='A named property', max_count=1, min_count=None, enum_values=None, class_id='', datatype='http://www.w3.org/2001/XMLSchema#string', pattern=''), Property(path='http://example.org/test-class/non-shape', varname='non-shape', comment='A class with no shape', max_count=1, min_count=None, enum_values=None, class_id='http://example.org/non-shape-class', datatype='', pattern=''), Property(path='http://example.org/test-class/nonnegative-integer-prop', varname='nonnegative-integer-prop', comment='a non-negative integer', max_count=1, min_count=None, enum_values=None, class_id='', datatype='http://www.w3.org/2001/XMLSchema#nonNegativeInteger', pattern=''), Property(path='http://example.org/test-class/positive-integer-prop', varname='positive-integer-prop', comment='A positive integer', max_count=1, min_count=None, enum_values=None, class_id='', datatype='http://www.w3.org/2001/XMLSchema#positiveInteger', pattern=''), Property(path='http://example.org/test-class/regex', varname='regex', comment='A regex validated string', max_count=1, min_count=None, enum_values=None, class_id='', datatype='http://www.w3.org/2001/XMLSchema#string', pattern='^foo\\d'), Property(path='http://example.org/test-class/regex-datetime', varname='regex-datetime', comment='A regex dateTime', max_count=1, min_count=None, enum_values=None, class_id='', datatype='http://www.w3.org/2001/XMLSchema#dateTime', pattern='^\\d\\d\\d\\d-\\d\\d-\\d\\dT\\d\\d:\\d\\d:\\d\\d\\+01:00$'), Property(path='http://example.org/test-class/regex-datetimestamp', varname='regex-datetimestamp', comment='A regex dateTimeStamp', max_count=1, min_count=None, enum_values=None, class_id='', datatype='http://www.w3.org/2001/XMLSchema#dateTimeStamp', pattern='^\\d\\d\\d\\d-\\d\\d-\\d\\dT\\d\\d:\\d\\d:\\d\\dZ$'), Property(path='http://example.org/test-class/regex-list', varname='regex-list', comment='A regex validated string list', max_count=None, min_count=None, enum_values=None, class_id='', datatype='http://www.w3.org/2001/XMLSchema#string', pattern='^foo\\d'), Property(path='http://example.org/test-class/string-list-no-datatype', varname='string-list-no-datatype', comment='A string list property with no sh:datatype', max_count=None, min_count=None, enum_values=None, class_id='', datatype='http://www.w3.org/2001/XMLSchema#string', pattern=''), Property(path='http://example.org/test-class/string-list-prop', varname='string-list-prop', comment='A string list property', max_count=None, min_count=None, enum_values=None, class_id='', datatype='http://www.w3.org/2001/XMLSchema#string', pattern=''), Property(path='http://example.org/test-class/string-scalar-prop', varname='string-scalar-prop', comment='A scalar string propery', max_count=1, min_count=None, enum_values=None, class_id='', datatype='http://www.w3.org/2001/XMLSchema#string', pattern='')], comment='The test class', id_property=None, node_kind=rdflib.term.URIRef('http://www.w3.org/ns/shacl#BlankNodeOrIRI'), is_extensible=False, is_abstract=False, named_individuals=[Individual(_id='http://example.org/test-class/named', varname='named', comment='')]) Class(_id='http://example.org/test-class-required', clsname=['http', '//example.org/test-class-required'], parent_ids=['http://example.org/test-class'], derived_ids=[], properties=[Property(path='http://example.org/test-class/required-string-list-prop', varname='required-string-list-prop', comment='A required string list property', max_count=2, min_count=1, enum_values=None, class_id='', datatype='http://www.w3.org/2001/XMLSchema#string', pattern=''), Property(path='http://example.org/test-class/required-string-scalar-prop', varname='required-string-scalar-prop', comment='A required scalar string property', max_count=1, min_count=1, enum_values=None, class_id='', datatype='http://www.w3.org/2001/XMLSchema#string', pattern='')], comment='', id_property=None, node_kind=rdflib.term.URIRef('http://www.w3.org/ns/shacl#BlankNodeOrIRI'), is_extensible=False, is_abstract=False, named_individuals=[]) Class(_id='http://example.org/test-derived-class', clsname=['http', '//example.org/test-derived-class'], parent_ids=['http://example.org/test-class'], derived_ids=[], properties=[Property(path='http://example.org/test-derived-class/string-prop', varname='string-prop', comment='A string property in a derived class', max_count=1, min_count=None, enum_values=None, class_id='', datatype='http://www.w3.org/2001/XMLSchema#string', pattern='')], comment='A class derived from test-class', id_property=None, node_kind=rdflib.term.URIRef('http://www.w3.org/ns/shacl#BlankNodeOrIRI'), is_extensible=False, is_abstract=False, named_individuals=[]) +Class(_id='http://example.org/uses-extensible-abstract-class', clsname=['http', '//example.org/uses-extensible-abstract-class'], parent_ids=[], derived_ids=[], properties=[Property(path='http://example.org/uses-extensible-abstract-class/prop', varname='prop', comment='A property that references and abstract extensible class', max_count=1, min_count=1, enum_values=None, class_id='http://example.org/extensible-abstract-class', datatype='', pattern='')], comment='A class that uses an abstract extensible class', id_property=None, node_kind=rdflib.term.URIRef('http://www.w3.org/ns/shacl#BlankNodeOrIRI'), is_extensible=False, is_abstract=False, named_individuals=[]) Class(_id='http://example.org/aaa-derived-class', clsname=['http', '//example.org/aaa-derived-class'], parent_ids=['http://example.org/parent-class'], derived_ids=[], properties=[], comment='Derived class that sorts before the parent to test ordering', id_property=None, node_kind=rdflib.term.URIRef('http://www.w3.org/ns/shacl#BlankNodeOrIRI'), is_extensible=False, is_abstract=False, named_individuals=[]) Class(_id='http://example.org/derived-node-kind-iri', clsname=['http', '//example.org/derived-node-kind-iri'], parent_ids=['http://example.org/node-kind-iri'], derived_ids=[], properties=[], comment='A class that derives its nodeKind from parent', id_property=None, node_kind=rdflib.term.URIRef('http://www.w3.org/ns/shacl#IRI'), is_extensible=False, is_abstract=False, named_individuals=[]) Class(_id='http://example.org/extensible-class', clsname=['http', '//example.org/extensible-class'], parent_ids=['http://example.org/link-class'], derived_ids=[], properties=[Property(path='http://example.org/extensible-class/property', varname='property', comment='An extensible property', max_count=1, min_count=None, enum_values=None, class_id='', datatype='http://www.w3.org/2001/XMLSchema#string', pattern=''), Property(path='http://example.org/extensible-class/required', varname='required', comment='A required extensible property', max_count=1, min_count=1, enum_values=None, class_id='', datatype='http://www.w3.org/2001/XMLSchema#string', pattern='')], comment='An extensible class', id_property=None, node_kind=rdflib.term.URIRef('http://www.w3.org/ns/shacl#BlankNodeOrIRI'), is_extensible=True, is_abstract=False, named_individuals=[]) diff --git a/tests/test_jsonschema.py b/tests/test_jsonschema.py index 18bf7ce6..045be72b 100644 --- a/tests/test_jsonschema.py +++ b/tests/test_jsonschema.py @@ -701,6 +701,38 @@ def node_kind_tests(name, blank, iri): "@type": "concrete-spdx-class", }, ), + # An extensible abstract class cannot be instantiated + ( + False, + { + "@context": CONTEXT, + "@type": "extensible-abstract-class", + }, + ), + # Any can type can be used where a extensible abstract class is + # references, except... (SEE NEXT) + ( + True, + { + "@context": CONTEXT, + "@type": "uses-extensible-abstract-class", + "uses-extensible-abstract-class/prop": { + "@type": "http://example.com/extended", + }, + }, + ), + # ... the exact type of the extensible abstract class is specifically + # not allowed + ( + False, + { + "@context": CONTEXT, + "@type": "uses-extensible-abstract-class", + "uses-extensible-abstract-class/prop": { + "@type": "extensible-abstract-class", + }, + }, + ), # Base object for type tests (True, BASE_OBJ), ], diff --git a/tests/test_python.py b/tests/test_python.py index 22b70392..90fa5168 100644 --- a/tests/test_python.py +++ b/tests/test_python.py @@ -1747,6 +1747,20 @@ def test_required_abstract_class_property(model, tmp_path): assert c.required_abstract_abstract_class_prop is None +def test_extensible_abstract_class(model): + @model.register("http://example.org/custom-extension-class") + class Extension(model.extensible_abstract_class): + pass + + # Test that an extensible abstract class cannot be created + with pytest.raises(NotImplementedError): + model.extensible_abstract_class() + + # Test that a class derived from an abstract extensible class can be + # created + Extension() + + def test_named_individual(model, roundtrip): objset = model.SHACLObjectSet() with roundtrip.open("r") as f: From a94f471d360fa53952e355d51848404f12c947cd Mon Sep 17 00:00:00 2001 From: Joshua Watt Date: Wed, 19 Jun 2024 12:23:17 -0600 Subject: [PATCH 09/14] Bump version for release --- src/shacl2code/version.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/shacl2code/version.py b/src/shacl2code/version.py index f8c83ce8..83c8ee31 100644 --- a/src/shacl2code/version.py +++ b/src/shacl2code/version.py @@ -1 +1 @@ -VERSION = "0.0.11" +VERSION = "0.0.12" From 8ebc0d475b9078e0139934e00bc84014e1ce6f64 Mon Sep 17 00:00:00 2001 From: Joshua Watt Date: Tue, 16 Jul 2024 16:04:33 -0600 Subject: [PATCH 10/14] Fix SHACL validation SHACL validation of the roundtip file wasn't actually occurring because the JSON-LD context file was incorrect. Fix many bugs discovered when making the SHACL model validate and also add a test to validate that the context is correct --- src/shacl2code/context.py | 9 ++-- src/shacl2code/lang/templates/python.j2 | 8 +-- tests/data/model/test-context.json | 66 ++++++++++++++++++++--- tests/data/model/test.ttl | 2 - tests/data/python/roundtrip.json | 6 +-- tests/expect/jsonschema/test-context.json | 24 ++++----- tests/expect/python/test-context.py | 40 +++++++------- tests/test_jsonschema.py | 10 ++-- tests/test_model_source.py | 58 ++++++++++++++++++++ tests/test_python.py | 19 +++++-- 10 files changed, 180 insertions(+), 62 deletions(-) diff --git a/src/shacl2code/context.py b/src/shacl2code/context.py index 85b91385..160ce52e 100644 --- a/src/shacl2code/context.py +++ b/src/shacl2code/context.py @@ -50,7 +50,7 @@ def compact_vocab(self, _id, vocab=None): else: v = self.__vocabs[-1] - return self.__compact_contexts(_id, v, self.__get_vocab_contexts()) + return self.__compact_contexts(_id, v, True) def __compact_contexts(self, _id, v="", apply_vocabs=False): if v not in self.__compacted or _id not in self.__compacted[v]: @@ -83,7 +83,8 @@ def collect_possible(_id): if apply_vocabs: possible |= remove_prefix(_id, value) elif name == "@base": - possible |= remove_prefix(_id, value) + if not apply_vocabs: + possible |= remove_prefix(_id, value) else: if isinstance(value, dict): value = value["@id"] @@ -91,7 +92,7 @@ def collect_possible(_id): if _id == value: possible.add(name) possible |= collect_possible(name) - elif _id.startswith(value): + elif _id.startswith(value) and value.endswith("/"): tmp_id = name + ":" + _id[len(value) :].lstrip("/") possible.add(tmp_id) possible |= collect_possible(tmp_id) @@ -130,7 +131,7 @@ def __expand_contexts(self, _id, v="", apply_vocabs=False): for ctx in contexts: for name, value in ctx.items(): - if name == "@base" and self.is_relative(_id): + if name == "@base" and self.is_relative(_id) and not apply_vocabs: _id = value + _id self.__expanded.setdefault(v, {})[_id] = self.__expand(_id, contexts) diff --git a/src/shacl2code/lang/templates/python.j2 b/src/shacl2code/lang/templates/python.j2 index 4ea8f7b6..a3faae51 100644 --- a/src/shacl2code/lang/templates/python.j2 +++ b/src/shacl2code/lang/templates/python.j2 @@ -1966,8 +1966,8 @@ CONTEXT_URLS = [ _NI_ENCODE_CONTEXT = { {%- for class in classes %} {%- for member in class.named_individuals %} - {%- if context.compact(member._id) != member._id %} - "{{ member._id }}": "{{ context.compact(member._id) }}", + {%- if context.compact_vocab(member._id) != member._id %} + "{{ member._id }}": "{{ context.compact_vocab(member._id) }}", {%- endif %} {%- endfor %} {%- endfor %} @@ -1976,8 +1976,8 @@ _NI_ENCODE_CONTEXT = { _NI_DECODE_CONTEXT = { {%- for class in classes %} {%- for member in class.named_individuals %} - {%- if context.compact(member._id) != member._id %} - "{{ context.compact(member._id) }}": "{{ member._id }}", + {%- if context.compact_vocab(member._id) != member._id %} + "{{ context.compact_vocab(member._id) }}": "{{ member._id }}", {%- endif %} {%- endfor %} {%- endfor %} diff --git a/tests/data/model/test-context.json b/tests/data/model/test-context.json index a01ee20d..ffe6beca 100644 --- a/tests/data/model/test-context.json +++ b/tests/data/model/test-context.json @@ -3,6 +3,30 @@ "@base": "http://example.org/", "test": "http://example.org/", "xsd": "http://www.w3.org/2001/XMLSchema#", + "aaa-derived-class": "test:aaa-derived-class", + "abstract-class": "test:abstract-class", + "abstract-spdx-class": "test:abstract-spdx-class", + "concrete-class": "test:concrete-class", + "concrete-spdx-class": "test:concrete-spdx-class", + "derived-node-kind-iri": "test:derived-node-kind-iri", + "enumType": "test:enumType", + "extensible-abstract-class": "test:extensible-abstract-class", + "extensible-class": "test:extensible-class", + "id-prop-class": "test:id-prop-class", + "inherited-id-prop-class": "test:inherited-id-prop-class", + "link-class": "test:link-class", + "link-derived-class": "test:link-derived-class", + "node-kind-blank": "test:node-kind-blank", + "node-kind-iri": "test:node-kind-iri", + "node-kind-iri-or-blank": "test:node-kind-iri-or-blank", + "non-shape-class": "test:non-shape-class", + "parent-class": "test:parent-class", + "required-abstract": "test:required-abstract", + "test-another-class": "test:test-another-class", + "test-class": "test:test-class", + "test-class-required": "test:test-class-required", + "test-derived-class": "test:test-derived-class", + "uses-extensible-abstract-class": "test:uses-extensible-abstract-class", "test-class/string-list-prop": { "@id": "test:test-class/string-list-prop", "@type": "xsd:string" @@ -77,17 +101,25 @@ }, "test-class/enum-prop": { "@id": "test:test-class/enum-prop", - "@type": "@id" + "@type": "@vocab", + "@context": { + "@vocab": "test:enumType/" + } }, "test-class/enum-list-prop": { "@id": "test:test-class/enum-list-prop", - "@type": "@id" + "@type": "@vocab", + "@context": { + "@vocab": "test:enumType/" + } }, "test-class/enum-prop-no-class": { "@id": "test:test-class/enum-prop-no-class", - "@type": "@id" + "@type": "@vocab", + "@context": { + "@vocab": "test:enumType/" + } }, - "test-class/named": "test:test-class/named", "test-class/regex": { "@id": "test:test-class/regex", "@type": "xsd:string" @@ -105,15 +137,15 @@ "@type": "xsd:dateTimeStamp" }, "link-class-link-prop": { - "@id": "test:link-class-prop", + "@id": "test:link-class-link-prop", "@type": "@id" }, "link-class-link-prop-no-class": { - "@id": "test:link-class-prop-no-class", + "@id": "test:link-class-link-prop-no-class", "@type": "@id" }, "link-class-link-list-prop": { - "@id": "test:link-class-list-prop", + "@id": "test:link-class-link-list-prop", "@type": "@id" }, "link-class-extensible": { @@ -135,6 +167,26 @@ "uses-extensible-abstract-class/prop": { "@id": "test:uses-extensible-abstract-class/prop", "@type": "@id" + }, + "import": { + "@id": "test:import", + "@type": "xsd:string" + }, + "encode": { + "@id": "test:encode", + "@type": "xsd:string" + }, + "test-class/non-shape": { + "@id": "test:test-class/non-shape", + "@type": "@id" + }, + "test-derived-class/string-prop": { + "@id": "test:test-derived-class/string-prop", + "@type": "xsd:string" + }, + "required-abstract/abstract-class-prop": { + "@id": "test:required-abstract/abstract-class-prop", + "@type": "@id" } } } diff --git a/tests/data/model/test.ttl b/tests/data/model/test.ttl index bcfd4605..ea2531d0 100644 --- a/tests/data/model/test.ttl +++ b/tests/data/model/test.ttl @@ -389,7 +389,6 @@ sh:path ; ], [ - sh:class ; sh:path ; sh:maxCount 1 ] @@ -524,7 +523,6 @@ a rdf:Class, sh:NodeShape, owl:Class ; rdfs:comment "A class that uses an abstract extensible class" ; sh:property [ - sh:class ; sh:path ; sh:minCount 1 ; sh:maxCount 1 diff --git a/tests/data/python/roundtrip.json b/tests/data/python/roundtrip.json index 30e16ad6..442f7718 100644 --- a/tests/data/python/roundtrip.json +++ b/tests/data/python/roundtrip.json @@ -62,8 +62,8 @@ "http://serialize.example.com/test-derived", "http://serialize.example.com/test" ], - "test-class/enum-prop": "enumType/foo", - "test-class/enum-prop-no-class": "enumType/bar", + "test-class/enum-prop": "foo", + "test-class/enum-prop-no-class": "bar", "test-class/regex": "foo1", "test-class/regex-datetime": "2024-03-11T00:00:00+01:00", "test-class/regex-datetimestamp": "2024-03-11T00:00:00Z", @@ -79,7 +79,7 @@ { "@type": "test-class", "@id": "http://serialize.example.com/test-named-individual-reference", - "test-class/class-prop": "test-class/named" + "test-class/class-prop": "test:test-class/named" }, { "@type": "test-class", diff --git a/tests/expect/jsonschema/test-context.json b/tests/expect/jsonschema/test-context.json index a7f4176d..5884b29b 100644 --- a/tests/expect/jsonschema/test-context.json +++ b/tests/expect/jsonschema/test-context.json @@ -913,26 +913,26 @@ }, "prop_testclass_testclassenumlistprop": { "enum": [ - "enumType/bar", - "enumType/foo", - "enumType/nolabel", - "enumType/non-named-individual" + "bar", + "foo", + "nolabel", + "non-named-individual" ] }, "prop_testclass_testclassenumprop": { "enum": [ - "enumType/bar", - "enumType/foo", - "enumType/nolabel", - "enumType/non-named-individual" + "bar", + "foo", + "nolabel", + "non-named-individual" ] }, "prop_testclass_testclassenumpropnoclass": { "enum": [ - "enumType/bar", - "enumType/foo", - "enumType/nolabel", - "enumType/non-named-individual" + "bar", + "foo", + "nolabel", + "non-named-individual" ] }, "prop_testclass_testclassfloatprop": { diff --git a/tests/expect/python/test-context.py b/tests/expect/python/test-context.py index 4a3d44d9..e98c16be 100755 --- a/tests/expect/python/test-context.py +++ b/tests/expect/python/test-context.py @@ -1950,17 +1950,17 @@ def callback(value, path): ] _NI_ENCODE_CONTEXT = { - "http://example.org/enumType/foo": "enumType/foo", - "http://example.org/enumType/bar": "enumType/bar", - "http://example.org/enumType/nolabel": "enumType/nolabel", - "http://example.org/test-class/named": "test-class/named", + "http://example.org/enumType/foo": "test:enumType/foo", + "http://example.org/enumType/bar": "test:enumType/bar", + "http://example.org/enumType/nolabel": "test:enumType/nolabel", + "http://example.org/test-class/named": "test:test-class/named", } _NI_DECODE_CONTEXT = { - "enumType/foo": "http://example.org/enumType/foo", - "enumType/bar": "http://example.org/enumType/bar", - "enumType/nolabel": "http://example.org/enumType/nolabel", - "test-class/named": "http://example.org/test-class/named", + "test:enumType/foo": "http://example.org/enumType/foo", + "test:enumType/bar": "http://example.org/enumType/bar", + "test:enumType/nolabel": "http://example.org/enumType/nolabel", + "test:test-class/named": "http://example.org/test-class/named", } @@ -2242,10 +2242,10 @@ def _register_props(cls): cls._add_property( "test_class_enum_list_prop", ListProp(EnumProp([ - ("http://example.org/enumType/bar", "enumType/bar"), - ("http://example.org/enumType/foo", "enumType/foo"), - ("http://example.org/enumType/nolabel", "enumType/nolabel"), - ("http://example.org/enumType/non-named-individual", "enumType/non-named-individual"), + ("http://example.org/enumType/bar", "bar"), + ("http://example.org/enumType/foo", "foo"), + ("http://example.org/enumType/nolabel", "nolabel"), + ("http://example.org/enumType/non-named-individual", "non-named-individual"), ])), iri="http://example.org/test-class/enum-list-prop", compact="test-class/enum-list-prop", @@ -2254,10 +2254,10 @@ def _register_props(cls): cls._add_property( "test_class_enum_prop", EnumProp([ - ("http://example.org/enumType/bar", "enumType/bar"), - ("http://example.org/enumType/foo", "enumType/foo"), - ("http://example.org/enumType/nolabel", "enumType/nolabel"), - ("http://example.org/enumType/non-named-individual", "enumType/non-named-individual"), + ("http://example.org/enumType/bar", "bar"), + ("http://example.org/enumType/foo", "foo"), + ("http://example.org/enumType/nolabel", "nolabel"), + ("http://example.org/enumType/non-named-individual", "non-named-individual"), ]), iri="http://example.org/test-class/enum-prop", compact="test-class/enum-prop", @@ -2266,10 +2266,10 @@ def _register_props(cls): cls._add_property( "test_class_enum_prop_no_class", EnumProp([ - ("http://example.org/enumType/bar", "enumType/bar"), - ("http://example.org/enumType/foo", "enumType/foo"), - ("http://example.org/enumType/nolabel", "enumType/nolabel"), - ("http://example.org/enumType/non-named-individual", "enumType/non-named-individual"), + ("http://example.org/enumType/bar", "bar"), + ("http://example.org/enumType/foo", "foo"), + ("http://example.org/enumType/nolabel", "nolabel"), + ("http://example.org/enumType/non-named-individual", "non-named-individual"), ]), iri="http://example.org/test-class/enum-prop-no-class", compact="test-class/enum-prop-no-class", diff --git a/tests/test_jsonschema.py b/tests/test_jsonschema.py index 045be72b..08a35bf2 100644 --- a/tests/test_jsonschema.py +++ b/tests/test_jsonschema.py @@ -836,8 +836,8 @@ def lst(v): *type_tests("test-class/named-property", good=["foo", ""], typ=[str]), *type_tests( "test-class/enum-prop", - good=["enumType/foo"], - bad=["foo"], + good=["foo"], + bad=["enumType/foo"], typ=[str], ), *type_tests( @@ -916,9 +916,9 @@ def lst(v): "test-class/enum-list-prop", good=[ [ - "enumType/foo", - "enumType/bar", - "enumType/nolabel", + "foo", + "bar", + "nolabel", ] ], bad=[ diff --git a/tests/test_model_source.py b/tests/test_model_source.py index f964e0fb..661f942b 100644 --- a/tests/test_model_source.py +++ b/tests/test_model_source.py @@ -7,6 +7,8 @@ import shutil import subprocess import pytest +import rdflib +import json from pathlib import Path import shacl2code @@ -391,3 +393,59 @@ def test_model_errors(file): str(CONTEXT_TEMPLATE), ] ) + + +def test_context_contents(): + from rdflib import RDF, OWL, RDFS + + model = rdflib.Graph() + model.parse(TEST_MODEL) + + with TEST_CONTEXT.open("r") as f: + context = json.load(f) + + def check_prefix(iri, typ): + nonlocal context + + test_prefix = context["@context"]["test"] + + assert iri.startswith( + test_prefix + ), f"{typ} '{str(iri)}' does not have correct prefix {test_prefix}" + + name = iri[len(test_prefix) :].lstrip("/") + + return name + + def check_subject(iri, typ): + nonlocal context + + name = check_prefix(iri, typ) + + assert name in context["@context"], f"{typ} '{name}' missing from context" + + value = context["@context"][name] + return name, value + + for c in model.subjects(RDF.type, OWL.Class): + name, value = check_subject(c, "Class") + assert value == f"test:{name}", f"Class '{name}' has bad value '{value}'" + + for p in model.subjects(RDF.type, RDF.Property): + name, value = check_subject(p, "Property") + + assert model.objects(p, RDFS.range), f"Property '{name}' is missing rdfs:range" + assert isinstance(value, dict), f"Property '{name}' must be a dictionary" + + assert "@id" in value, f"Property '{name}' missing @id" + assert "@type" in value, f"Property '{name}' missing @type" + assert ( + value["@id"] == f"test:{name}" + ), f"Context '{name}' has bad @id '{value['@id']}'" + + for i in model.subjects(RDF.type, OWL.NamedIndividual): + name = check_prefix(i, "Named Individual") + + assert ( + name not in context + ), f"Named Individual '{name}' should not be in context" diff --git a/tests/test_python.py b/tests/test_python.py index 90fa5168..913751d6 100644 --- a/tests/test_python.py +++ b/tests/test_python.py @@ -1478,7 +1478,7 @@ class OpenExtension(model.extensible_class): obj[TEST_IRI] = "foo" -def test_named_individuals(model): +def test_enum_named_individuals(model): assert type(model.enumType.foo) is str assert model.enumType.foo == "http://example.org/enumType/foo" @@ -1565,16 +1565,25 @@ def test_iri(model, roundtrip): def test_shacl(roundtrip): - model = rdflib.Graph() - model.parse(TEST_MODEL) + from rdflib import RDF, URIRef data = rdflib.Graph() data.parse(roundtrip) + # We need to add the referenced non-shape object, otherwise SHACL will + # complain it is missing + data.add( + ( + URIRef("http://serialize.example.com/non-shape"), + RDF.type, + URIRef("http://example.org/non-shape-class"), + ) + ) + conforms, result_graph, result_text = pyshacl.validate( data, - shacl_graph=model, - ont_graph=model, + shacl_graph=str(TEST_MODEL), + ont_graph=str(TEST_MODEL), ) assert conforms, result_text From eeef7e071ea7fa172a11e0f28f7c8b34d032a86b Mon Sep 17 00:00:00 2001 From: Joshua Watt Date: Tue, 23 Jul 2024 12:34:46 -0600 Subject: [PATCH 11/14] Fix context handling The way that contexts were dealt with was incorrect in a lot of ways, particularly when dealing with global named individuals. Correct all of these problems and add better test cases for context expansion and compaction. The new test suite uses `jsonld-cli` from `npm` to validate the expansion and compaction, which is the same code used by the official JSON-LD documentation in the JSON-LD playground. This makes it so that named individuals can be correctly handled, as long as the property that needs to reference them is `"@type": "@vocab"` in the context. --- .github/workflows/test.yaml | 5 + src/shacl2code/context.py | 329 +++++++++++++------- src/shacl2code/lang/common.py | 9 + src/shacl2code/lang/templates/jsonschema.j2 | 10 +- src/shacl2code/lang/templates/python.j2 | 95 +++--- src/shacl2code/model.py | 2 +- tests/data/context.j2 | 4 +- tests/data/model/test-context.json | 3 +- tests/data/python/roundtrip.json | 2 +- tests/expect/jsonschema/test-context.json | 10 +- tests/expect/python/test-context.py | 77 ++--- tests/expect/python/test.py | 57 ++-- tests/test_context.py | 222 +++++++------ tests/test_jsonschema.py | 2 +- 14 files changed, 477 insertions(+), 350 deletions(-) diff --git a/.github/workflows/test.yaml b/.github/workflows/test.yaml index fa68d95f..fbb0af7b 100644 --- a/.github/workflows/test.yaml +++ b/.github/workflows/test.yaml @@ -22,10 +22,15 @@ jobs: uses: actions/setup-python@v4 with: python-version: ${{ matrix.python-version }} + - name: Setup Node + uses: actions/setup-node@v4 + with: + node-version: 20 - name: Install dependencies run: | python -m pip install --upgrade pip pip install build + npm install -g jsonld-cli - name: Build package run: | python -m build diff --git a/src/shacl2code/context.py b/src/shacl2code/context.py index 160ce52e..1551c2cc 100644 --- a/src/shacl2code/context.py +++ b/src/shacl2code/context.py @@ -2,15 +2,26 @@ # # SPDX-License-Identifier: MIT +import re +from contextlib import contextmanager + + +def foreach_context(contexts): + for ctx in contexts: + for name, value in ctx.items(): + yield name, value -class Context(object): - from contextlib import contextmanager +class Context(object): def __init__(self, contexts=[]): self.contexts = [c for c in contexts if c] self.__vocabs = [] - self.__expanded = {} - self.__compacted = {} + self.__expanded_iris = {} + self.__expanded_ids = {} + self.__expanded_vocabs = {} + self.__compacted_iris = {} + self.__compacted_ids = {} + self.__compacted_vocabs = {} @contextmanager def vocab_push(self, vocab): @@ -24,145 +35,243 @@ def vocab_push(self, vocab): finally: self.__vocabs.pop() + def __vocab_key(self): + if not self.__vocabs: + return "" + + return self.__vocabs[-1] + def __get_vocab_contexts(self): contexts = [] for v in self.__vocabs: - for ctx in self.contexts: - # Check for vocabulary contexts - for name, value in ctx.items(): - if ( - isinstance(value, dict) - and value["@type"] == "@vocab" - and v == self.__expand(value["@id"], self.contexts) - ): + for name, value in foreach_context(self.contexts): + if ( + isinstance(value, dict) + and value["@type"] == "@vocab" + and v == self.expand_iri(value["@id"]) + ): + if "@context" in value: contexts.insert(0, value["@context"]) - return contexts - - def compact(self, _id): - return self.__compact_contexts(_id) + contexts.extend(self.contexts) - def compact_vocab(self, _id, vocab=None): - with self.vocab_push(vocab): - if not self.__vocabs: - v = "" - else: - v = self.__vocabs[-1] - - return self.__compact_contexts(_id, v, True) - - def __compact_contexts(self, _id, v="", apply_vocabs=False): - if v not in self.__compacted or _id not in self.__compacted[v]: - if apply_vocabs: - contexts = self.__get_vocab_contexts() + self.contexts - else: - contexts = self.contexts - - self.__compacted.setdefault(v, {})[_id] = self.__compact( - _id, - contexts, - apply_vocabs, - ) - return self.__compacted[v][_id] + return contexts - def __compact(self, _id, contexts, apply_vocabs): + def __choose_possible( + self, + term, + default, + contexts, + *, + vocab=False, + base=False, + exact=False, + prefix=False, + ): def remove_prefix(_id, value): + expanded_id = self.expand_iri(_id) + expanded_value = self.expand_iri(value) + possible = set() - if _id.startswith(value): - tmp_id = _id[len(value) :] + if expanded_id.startswith(expanded_value): + tmp_id = _id[len(expanded_value) :] possible.add(tmp_id) - possible |= collect_possible(tmp_id) return possible - def collect_possible(_id): + def helper(term): possible = set() - for ctx in contexts: - for name, value in ctx.items(): - if name == "@vocab": - if apply_vocabs: - possible |= remove_prefix(_id, value) - elif name == "@base": - if not apply_vocabs: - possible |= remove_prefix(_id, value) - else: - if isinstance(value, dict): - value = value["@id"] - - if _id == value: - possible.add(name) - possible |= collect_possible(name) - elif _id.startswith(value) and value.endswith("/"): - tmp_id = name + ":" + _id[len(value) :].lstrip("/") - possible.add(tmp_id) - possible |= collect_possible(tmp_id) + for name, value in foreach_context(contexts): + if name == "@vocab": + if vocab: + possible |= remove_prefix(term, value) + continue + + if name == "@base": + if base: + possible |= remove_prefix(term, value) + continue + + if isinstance(value, dict): + value = value["@id"] + + if term == self.expand_iri(value): + if exact and name not in possible: + possible.add(name) + possible |= helper(name) + continue + + if not prefix: + continue + + if term.startswith(value) and value.endswith("/"): + tmp_id = name + ":" + term[len(value) :].lstrip("/") + if tmp_id not in possible: + possible.add(tmp_id) + possible |= helper(tmp_id) + continue + + if term.startswith(value + ":") and self.expand_iri(value).endswith( + "/" + ): + tmp_id = name + term[len(value) :] + if tmp_id not in possible: + possible.add(tmp_id) + possible |= helper(tmp_id) + continue return possible - possible = collect_possible(_id) + possible = helper(term) + if not possible: - return _id + return default # To select from the possible identifiers, choose the one that has the # least context (fewest ":"), then the shortest, and finally # alphabetically possible = list(possible) possible.sort(key=lambda p: (p.count(":"), len(p), p)) - return possible[0] - def is_relative(self, _id): - import re - - return not re.match(r"[^:]+:", _id) - - def __expand_contexts(self, _id, v="", apply_vocabs=False): - if v not in self.__expanded or _id not in self.__expanded[v]: - if apply_vocabs: - contexts = self.__get_vocab_contexts() + self.contexts - - # Apply contexts - for ctx in contexts: - for name, value in ctx.items(): - if name == "@vocab": - _id = value + _id - else: - contexts = self.contexts + def compact_iri(self, iri): + if iri not in self.__compacted_iris: + self.__compacted_iris[iri] = self.__choose_possible( + iri, + iri, + self.contexts, + exact=True, + prefix=True, + ) - for ctx in contexts: - for name, value in ctx.items(): - if name == "@base" and self.is_relative(_id) and not apply_vocabs: - _id = value + _id + return self.__compacted_iris[iri] - self.__expanded.setdefault(v, {})[_id] = self.__expand(_id, contexts) + def compact_id(self, _id): + if ":" not in _id: + return _id - return self.__expanded[v][_id] + if _id not in self.__compacted_ids: + self.__compacted_ids[_id] = self.__choose_possible( + _id, + _id, + self.contexts, + base=True, + prefix=True, + ) - def expand(self, _id): - return self.__expand_contexts(_id) + return self.__compacted_ids[_id] - def expand_vocab(self, _id, vocab=""): + def compact_vocab(self, term, vocab=None): with self.vocab_push(vocab): - if not self.__vocabs: - v = "" - else: - v = self.__vocabs[-1] - - return self.__expand_contexts(_id, v, True) + v = self.__vocab_key() + if v in self.__compacted_vocabs and term in self.__compacted_vocabs[v]: + return self.__compacted_vocabs[v][term] + + compact = self.__choose_possible( + term, + None, + self.__get_vocab_contexts(), + vocab=True, + exact=True, + ) + if compact is not None: + self.__compacted_vocabs.setdefault(v, {})[term] = self.compact_id( + compact + ) + return compact + + # If unable to compact with a vocabulary, compact as an ID + return self.compact_id(term) + + def expand_iri(self, iri): + if iri not in self.__expanded_iris: + self.__expanded_iris[iri] = self.__expand( + iri, + self.contexts, + exact=True, + prefix=True, + ) - def __expand(self, _id, contexts): - for ctx in contexts: - if ":" not in _id: - if _id in ctx: - if isinstance(ctx[_id], dict): - return self.__expand(ctx[_id]["@id"], contexts) - return self.__expand(ctx[_id], contexts) - continue + return self.__expanded_iris[iri] - prefix, suffix = _id.split(":", 1) - if prefix not in ctx: - continue + def expand_id(self, _id): + if _id not in self.__expanded_ids: + self.__expanded_ids[_id] = self.__expand( + _id, + self.contexts, + base=True, + prefix=True, + ) - return self.__expand(prefix, contexts) + suffix + return self.__expanded_ids[_id] - return _id + def expand_vocab(self, term, vocab=None): + with self.vocab_push(vocab): + v = self.__vocab_key() + if v not in self.__expanded_vocabs or term not in self.__expanded_vocabs[v]: + value = self.__expand( + term, + self.__get_vocab_contexts(), + vocab=True, + exact=True, + ) + self.__expanded_vocabs.setdefault(v, {})[term] = self.expand_id(value) + + return self.__expanded_vocabs[v][term] + + def __expand( + self, + term, + contexts, + *, + base=False, + exact=False, + prefix=False, + vocab=False, + ): + def helper(term): + vocabs = [] + bases = [] + prefixes = [] + exacts = [] + is_short = not re.match(r"[^:]+:", term) + + for name, value in foreach_context(contexts): + if name == "@vocab": + if vocab and is_short: + vocabs.append(helper(value)) + continue + + if name == "@base": + if base and is_short: + bases.append(value) + continue + + if isinstance(value, dict): + value = value["@id"] + + if term == name: + if exact: + exacts.append(helper(value)) + continue + + if prefix: + prefixes.append(name) + + for e in exacts: + return e + + if ":" in term: + p, suffix = term.split(":", 1) + for name in prefixes: + if p == name: + p = self.expand_iri(p) + if p.endswith("/"): + return p + suffix + + for value in vocabs + bases: + return value + term + + return term + + return helper(term) diff --git a/src/shacl2code/lang/common.py b/src/shacl2code/lang/common.py index c5abeac2..eb9a1ccb 100644 --- a/src/shacl2code/lang/common.py +++ b/src/shacl2code/lang/common.py @@ -114,6 +114,14 @@ def _recurse(cls): d.sort() return d + def get_all_named_individuals(cls): + ni = set(i._id for i in cls.named_individuals) + + for d in get_all_derived(cls): + ni |= set(i._id for i in classes.get(d).named_individuals) + + return ni + classes = ObjectList(model.classes) concrete_classes = ObjectList( list(c for c in model.classes if not c.is_abstract) @@ -132,6 +140,7 @@ def _recurse(cls): env = { "get_all_derived": get_all_derived, + "get_all_named_individuals": get_all_named_individuals, **self.get_extra_env(), } diff --git a/src/shacl2code/lang/templates/jsonschema.j2 b/src/shacl2code/lang/templates/jsonschema.j2 index ecce794e..912bf3da 100644 --- a/src/shacl2code/lang/templates/jsonschema.j2 +++ b/src/shacl2code/lang/templates/jsonschema.j2 @@ -69,7 +69,7 @@ {%- endif %} "properties": { "{{ class.id_property or "@id" }}": { "$ref": "#/$defs/{{ class.node_kind.split("#")[-1] }}" }, - "{{ context.compact("@type") }}": { + "{{ context.compact_iri("@type") }}": { {#- Abstract Extensible classes are weird; any type _except_ the specific class type is allowed #} {%- if class.is_abstract and class.is_extensible %} "allOf": [ @@ -98,8 +98,8 @@ {%- for d in get_all_derived(class) + [class._id] %} {%- set ns.json_refs = ns.json_refs + ["#/$defs/" + varname(*classes.get(d).clsname)] %} {%- for n in classes.get(d).named_individuals %} - {%- if context.compact(n._id) != n._id %} - {%- set ns.named_individuals = ns.named_individuals + [context.compact(n._id)] %} + {%- if context.compact_iri(n._id) != n._id %} + {%- set ns.named_individuals = ns.named_individuals + [context.compact_iri(n._id)] %} {%- endif %} {%- endfor %} {%- endfor %} @@ -267,7 +267,7 @@ "SHACLClass": { "type": "object", "properties": { - "{{ context.compact("@type") }}": { + "{{ context.compact_iri("@type") }}": { "oneOf": [ { "$ref": "#/$defs/IRI" }, { @@ -280,7 +280,7 @@ ] } }, - "required": ["{{ context.compact("@type") }}"] + "required": ["{{ context.compact_iri("@type") }}"] }, "AnyClass": { "anyOf": [ diff --git a/src/shacl2code/lang/templates/python.j2 b/src/shacl2code/lang/templates/python.j2 index a3faae51..d55bc654 100644 --- a/src/shacl2code/lang/templates/python.j2 +++ b/src/shacl2code/lang/templates/python.j2 @@ -220,13 +220,34 @@ class FloatProp(Property): return decoder.read_float() -class ObjectProp(Property): +class IRIProp(Property): + def __init__(self, context=[], *, pattern=None): + super().__init__(pattern=pattern) + self.context = context + + def compact(self, value): + for iri, compact in self.context: + if value == iri: + return compact + return None + + def expand(self, value): + for iri, compact in self.context: + if value == compact: + return iri + return None + + def iri_values(self): + return (iri for iri, _ in self.context) + + +class ObjectProp(IRIProp): """ A scalar SHACL object property of a SHACL object """ - def __init__(self, cls, required): - super().__init__() + def __init__(self, cls, required, context=[]): + super().__init__(context) self.cls = cls self.required = required @@ -264,8 +285,7 @@ class ObjectProp(Property): raise ValueError("Object cannot be None") if isinstance(value, str): - value = _NI_ENCODE_CONTEXT.get(value, value) - encoder.write_iri(value) + encoder.write_iri(value, self.compact(value)) return return value.encode(encoder, state) @@ -275,7 +295,7 @@ class ObjectProp(Property): if iri is None: return self.cls.decode(decoder, objectset=objectset) - iri = _NI_DECODE_CONTEXT.get(iri, iri) + iri = self.expand(iri) or iri if objectset is None: return iri @@ -445,36 +465,27 @@ class ListProp(Property): return ListProxy(self.prop, data=data) -class EnumProp(Property): +class EnumProp(IRIProp): VALID_TYPES = str def __init__(self, values, *, pattern=None): - super().__init__(pattern=pattern) - self.values = values + super().__init__(values, pattern=pattern) def validate(self, value): super().validate(value) - valid_values = (iri for iri, _ in self.values) + valid_values = self.iri_values() if value not in valid_values: raise ValueError( f"'{value}' is not a valid value. Choose one of {' '.join(valid_values)}" ) def encode(self, encoder, value, state): - for iri, compact in self.values: - if iri == value: - encoder.write_enum(value, self, compact) - return - - encoder.write_enum(value, self) + encoder.write_enum(value, self, self.compact(value)) def decode(self, decoder, *, objectset=None): v = decoder.read_enum(self) - for iri, compact in self.values: - if v == compact: - return iri - return v + return self.expand(v) or v class NodeKind(Enum): @@ -1518,14 +1529,14 @@ class JSONLDDecoder(Decoder): def object_keys(self): for key in self.data.keys(): - if key in ("@type", "{{ context.compact('@type') }}"): + if key in ("@type", "{{ context.compact_iri('@type') }}"): continue if self.root and key == "@context": continue yield key def read_object(self): - typ = self.__get_value("@type", "{{ context.compact('@type') }}") + typ = self.__get_value("@type", "{{ context.compact_iri('@type') }}") if typ is not None: return typ, self @@ -1717,7 +1728,7 @@ class JSONLDEncoder(Encoder): @contextmanager def write_object(self, o, _id, needs_id): self.data = { - "{{ context.compact('@type') }}": o.COMPACT_TYPE or o.TYPE, + "{{ context.compact_iri('@type') }}": o.COMPACT_TYPE or o.TYPE, } if needs_id: self.data[o.ID_ALIAS or "@id"] = _id @@ -1841,7 +1852,7 @@ class JSONLDInlineEncoder(Encoder): self._write_comma() self.write("{") - self.write_string("{{ context.compact('@type') }}") + self.write_string("{{ context.compact_iri('@type') }}") self.write(":") self.write_string(o.COMPACT_TYPE or o.TYPE) self.comma = True @@ -1963,26 +1974,6 @@ CONTEXT_URLS = [ {%- endfor %} ] -_NI_ENCODE_CONTEXT = { -{%- for class in classes %} -{%- for member in class.named_individuals %} - {%- if context.compact_vocab(member._id) != member._id %} - "{{ member._id }}": "{{ context.compact_vocab(member._id) }}", - {%- endif %} -{%- endfor %} -{%- endfor %} -} - -_NI_DECODE_CONTEXT = { -{%- for class in classes %} -{%- for member in class.named_individuals %} - {%- if context.compact_vocab(member._id) != member._id %} - "{{ context.compact_vocab(member._id) }}": "{{ member._id }}", - {%- endif %} -{%- endfor %} -{%- endfor %} -} - # CLASSES {%- for class in classes %} @@ -1991,7 +1982,7 @@ _NI_DECODE_CONTEXT = { #{{ (" " + l).rstrip() }} {%- endfor %} {%- endif %} -@register("{{ class._id }}"{%- if context.compact(class._id) != class._id %}, compact_type="{{ context.compact(class._id) }}"{%- endif %}, abstract={{ class.is_abstract }}) +@register("{{ class._id }}"{%- if context.compact_iri(class._id) != class._id %}, compact_type="{{ context.compact_iri(class._id) }}"{%- endif %}, abstract={{ class.is_abstract }}) class {{ varname(*class.clsname) }}( {%- if class.is_extensible -%} SHACLExtensibleObject{{", "}} @@ -2042,7 +2033,19 @@ class {{ varname(*class.clsname) }}( {%- endfor %} ]) {%- elif prop.class_id -%} - ObjectProp({{ varname(*classes.get(prop.class_id).clsname) }}, {% if prop.min_count and not is_list %}True{% else %}False{% endif %}) + {%- set ctx = [] %} + {%- for value in get_all_named_individuals(classes.get(prop.class_id)) %} + {%- if context.compact_vocab(value, prop.path) != value %} + {{- ctx.append((value, context.compact_vocab(value, prop.path))) or "" }} + {%- endif %} + {%- endfor -%} + ObjectProp({{ varname(*classes.get(prop.class_id).clsname) }}, {% if prop.min_count and not is_list %}True{% else %}False{% endif %}{% if ctx %}, context=[ + {%- for value, compact in ctx %} + ("{{ value }}", "{{ compact }}"), + {%- endfor %} + ], + {%- endif -%} + ) {%- else -%} {% if not prop.datatype in DATATYPE_CLASSES -%} {{ abort("Unknown data type " + prop.datatype) -}} diff --git a/src/shacl2code/model.py b/src/shacl2code/model.py index f31ce99a..22f05fe4 100644 --- a/src/shacl2code/model.py +++ b/src/shacl2code/model.py @@ -280,7 +280,7 @@ def get_compact_id(self, _id, *, fallback=None): """ _id = str(_id) if _id not in self.compact_ids: - self.compact_ids[_id] = self.context.compact(_id) + self.compact_ids[_id] = self.context.compact_iri(_id) if self.compact_ids[_id] == _id and fallback is not None: return fallback diff --git a/tests/data/context.j2 b/tests/data/context.j2 index e4dd73f1..ba743325 100644 --- a/tests/data/context.j2 +++ b/tests/data/context.j2 @@ -2,8 +2,8 @@ Context: {{ url }} {% endfor -%} {% for enum in enums %} -{{ enum._id }}: {{ context.compact(enum._id) }} +{{ enum._id }}: {{ context.compact_iri(enum._id) }} {% endfor %} {% for class in classes %} -{{ class._id }}: {{ context.compact(class._id) }} +{{ class._id }}: {{ context.compact_iri(class._id) }} {% endfor %} diff --git a/tests/data/model/test-context.json b/tests/data/model/test-context.json index ffe6beca..e712ea27 100644 --- a/tests/data/model/test-context.json +++ b/tests/data/model/test-context.json @@ -27,6 +27,7 @@ "test-class-required": "test:test-class-required", "test-derived-class": "test:test-derived-class", "uses-extensible-abstract-class": "test:uses-extensible-abstract-class", + "named": "test:test-class/named", "test-class/string-list-prop": { "@id": "test:test-class/string-list-prop", "@type": "xsd:string" @@ -89,7 +90,7 @@ }, "test-class/class-prop": { "@id": "test:test-class/class-prop", - "@type": "@id" + "@type": "@vocab" }, "test-class/class-prop-no-class": { "@id": "test:test-class/class-prop-no-class", diff --git a/tests/data/python/roundtrip.json b/tests/data/python/roundtrip.json index 442f7718..414319a7 100644 --- a/tests/data/python/roundtrip.json +++ b/tests/data/python/roundtrip.json @@ -79,7 +79,7 @@ { "@type": "test-class", "@id": "http://serialize.example.com/test-named-individual-reference", - "test-class/class-prop": "test:test-class/named" + "test-class/class-prop": "named" }, { "@type": "test-class", diff --git a/tests/expect/jsonschema/test-context.json b/tests/expect/jsonschema/test-context.json index 5884b29b..5060d83a 100644 --- a/tests/expect/jsonschema/test-context.json +++ b/tests/expect/jsonschema/test-context.json @@ -178,9 +178,9 @@ { "$ref": "#/$defs/enumType" } ] }, - { "const": "enumType/foo" }, - { "const": "enumType/bar" }, - { "const": "enumType/nolabel" }, + { "const": "test:enumType/foo" }, + { "const": "test:enumType/bar" }, + { "const": "test:enumType/nolabel" }, { "$ref": "#/$defs/BlankNodeOrIRI" } ] }, @@ -606,7 +606,7 @@ { "$ref": "#/$defs/parentclass" } ] }, - { "const": "test-class/named" }, + { "const": "named" }, { "$ref": "#/$defs/BlankNodeOrIRI" } ] }, @@ -732,7 +732,7 @@ { "$ref": "#/$defs/testclass" } ] }, - { "const": "test-class/named" }, + { "const": "named" }, { "$ref": "#/$defs/BlankNodeOrIRI" } ] }, diff --git a/tests/expect/python/test-context.py b/tests/expect/python/test-context.py index e98c16be..361cbe36 100755 --- a/tests/expect/python/test-context.py +++ b/tests/expect/python/test-context.py @@ -220,13 +220,34 @@ def decode(self, decoder, *, objectset=None): return decoder.read_float() -class ObjectProp(Property): +class IRIProp(Property): + def __init__(self, context=[], *, pattern=None): + super().__init__(pattern=pattern) + self.context = context + + def compact(self, value): + for iri, compact in self.context: + if value == iri: + return compact + return None + + def expand(self, value): + for iri, compact in self.context: + if value == compact: + return iri + return None + + def iri_values(self): + return (iri for iri, _ in self.context) + + +class ObjectProp(IRIProp): """ A scalar SHACL object property of a SHACL object """ - def __init__(self, cls, required): - super().__init__() + def __init__(self, cls, required, context=[]): + super().__init__(context) self.cls = cls self.required = required @@ -264,8 +285,7 @@ def encode(self, encoder, value, state): raise ValueError("Object cannot be None") if isinstance(value, str): - value = _NI_ENCODE_CONTEXT.get(value, value) - encoder.write_iri(value) + encoder.write_iri(value, self.compact(value)) return return value.encode(encoder, state) @@ -275,7 +295,7 @@ def decode(self, decoder, *, objectset=None): if iri is None: return self.cls.decode(decoder, objectset=objectset) - iri = _NI_DECODE_CONTEXT.get(iri, iri) + iri = self.expand(iri) or iri if objectset is None: return iri @@ -445,36 +465,27 @@ def decode(self, decoder, *, objectset=None): return ListProxy(self.prop, data=data) -class EnumProp(Property): +class EnumProp(IRIProp): VALID_TYPES = str def __init__(self, values, *, pattern=None): - super().__init__(pattern=pattern) - self.values = values + super().__init__(values, pattern=pattern) def validate(self, value): super().validate(value) - valid_values = (iri for iri, _ in self.values) + valid_values = self.iri_values() if value not in valid_values: raise ValueError( f"'{value}' is not a valid value. Choose one of {' '.join(valid_values)}" ) def encode(self, encoder, value, state): - for iri, compact in self.values: - if iri == value: - encoder.write_enum(value, self, compact) - return - - encoder.write_enum(value, self) + encoder.write_enum(value, self, self.compact(value)) def decode(self, decoder, *, objectset=None): v = decoder.read_enum(self) - for iri, compact in self.values: - if v == compact: - return iri - return v + return self.expand(v) or v class NodeKind(Enum): @@ -1949,20 +1960,6 @@ def callback(value, path): "https://spdx.github.io/spdx-3-model/context.json", ] -_NI_ENCODE_CONTEXT = { - "http://example.org/enumType/foo": "test:enumType/foo", - "http://example.org/enumType/bar": "test:enumType/bar", - "http://example.org/enumType/nolabel": "test:enumType/nolabel", - "http://example.org/test-class/named": "test:test-class/named", -} - -_NI_DECODE_CONTEXT = { - "test:enumType/foo": "http://example.org/enumType/foo", - "test:enumType/bar": "http://example.org/enumType/bar", - "test:enumType/nolabel": "http://example.org/enumType/nolabel", - "test:test-class/named": "http://example.org/test-class/named", -} - # CLASSES # An Abstract class @@ -2199,21 +2196,27 @@ def _register_props(cls): # A test-class list property cls._add_property( "test_class_class_list_prop", - ListProp(ObjectProp(test_class, False)), + ListProp(ObjectProp(test_class, False, context=[ + ("http://example.org/test-class/named", "named"), + ],)), iri="http://example.org/test-class/class-list-prop", compact="test-class/class-list-prop", ) # A test-class property cls._add_property( "test_class_class_prop", - ObjectProp(test_class, False), + ObjectProp(test_class, False, context=[ + ("http://example.org/test-class/named", "named"), + ],), iri="http://example.org/test-class/class-prop", compact="test-class/class-prop", ) # A test-class property with no sh:class cls._add_property( "test_class_class_prop_no_class", - ObjectProp(test_class, False), + ObjectProp(test_class, False, context=[ + ("http://example.org/test-class/named", "named"), + ],), iri="http://example.org/test-class/class-prop-no-class", compact="test-class/class-prop-no-class", ) diff --git a/tests/expect/python/test.py b/tests/expect/python/test.py index cd689f7b..12421633 100644 --- a/tests/expect/python/test.py +++ b/tests/expect/python/test.py @@ -220,13 +220,34 @@ def decode(self, decoder, *, objectset=None): return decoder.read_float() -class ObjectProp(Property): +class IRIProp(Property): + def __init__(self, context=[], *, pattern=None): + super().__init__(pattern=pattern) + self.context = context + + def compact(self, value): + for iri, compact in self.context: + if value == iri: + return compact + return None + + def expand(self, value): + for iri, compact in self.context: + if value == compact: + return iri + return None + + def iri_values(self): + return (iri for iri, _ in self.context) + + +class ObjectProp(IRIProp): """ A scalar SHACL object property of a SHACL object """ - def __init__(self, cls, required): - super().__init__() + def __init__(self, cls, required, context=[]): + super().__init__(context) self.cls = cls self.required = required @@ -264,8 +285,7 @@ def encode(self, encoder, value, state): raise ValueError("Object cannot be None") if isinstance(value, str): - value = _NI_ENCODE_CONTEXT.get(value, value) - encoder.write_iri(value) + encoder.write_iri(value, self.compact(value)) return return value.encode(encoder, state) @@ -275,7 +295,7 @@ def decode(self, decoder, *, objectset=None): if iri is None: return self.cls.decode(decoder, objectset=objectset) - iri = _NI_DECODE_CONTEXT.get(iri, iri) + iri = self.expand(iri) or iri if objectset is None: return iri @@ -445,36 +465,27 @@ def decode(self, decoder, *, objectset=None): return ListProxy(self.prop, data=data) -class EnumProp(Property): +class EnumProp(IRIProp): VALID_TYPES = str def __init__(self, values, *, pattern=None): - super().__init__(pattern=pattern) - self.values = values + super().__init__(values, pattern=pattern) def validate(self, value): super().validate(value) - valid_values = (iri for iri, _ in self.values) + valid_values = self.iri_values() if value not in valid_values: raise ValueError( f"'{value}' is not a valid value. Choose one of {' '.join(valid_values)}" ) def encode(self, encoder, value, state): - for iri, compact in self.values: - if iri == value: - encoder.write_enum(value, self, compact) - return - - encoder.write_enum(value, self) + encoder.write_enum(value, self, self.compact(value)) def decode(self, decoder, *, objectset=None): v = decoder.read_enum(self) - for iri, compact in self.values: - if v == compact: - return iri - return v + return self.expand(v) or v class NodeKind(Enum): @@ -1948,12 +1959,6 @@ def callback(value, path): CONTEXT_URLS = [ ] -_NI_ENCODE_CONTEXT = { -} - -_NI_DECODE_CONTEXT = { -} - # CLASSES # An Abstract class diff --git a/tests/test_context.py b/tests/test_context.py index 08e62ffc..599d7e09 100644 --- a/tests/test_context.py +++ b/tests/test_context.py @@ -4,141 +4,133 @@ # SPDX-License-Identifier: MIT import pytest +import json +import subprocess from shacl2code.context import Context TEST_CONTEXTS = [ { - "foo": "http://bar/", - "foobat": "foo:bat", - "idfoo": { - "@id": "http://idbar/", + "root": "http://root/", + "rootPrefix1": "root:prefix1/", + "rootPrefix2": "rootPrefix1:prefix2/", + "rootTerminal1": "root:terminal", + "rootTerminal2": "rootTerminal1:terminal2", + "idProperty": { + "@id": "root:property", "@type": "@id", }, - "idfoobat": { - "@id": "idfoo:bat", - "@type": "@id", - }, - "v": { + "vocabProperty": { "@type": "@vocab", - "@id": "foo:vocab", + "@id": "root:vocab", "@context": { - "@vocab": "foo:prefix/", + "@vocab": "root:vocabPrefix/", }, }, - "idfoostring": { - "@id": "idfoo:string", - "@type": "http://www.w3.org/2001/XMLSchema#string", + "rootVocabProperty": { + "@type": "@vocab", + "@id": "root:rootVocab", }, - }, + "named": "root:named", + } ] BASE_CONTEXT = [ { - "@base": "http://bar/", + "@base": "http://base/", }, ] @pytest.mark.parametrize( - "context,compact_id,expand_id", - [ - (TEST_CONTEXTS, "nonexist", "nonexist"), - (TEST_CONTEXTS, "foo", "http://bar/"), - (TEST_CONTEXTS, "foo:baz", "http://bar/baz"), - (TEST_CONTEXTS, "foobat", "http://bar/bat"), - (TEST_CONTEXTS, "idfoo", "http://idbar/"), - (TEST_CONTEXTS, "idfoobat", "http://idbar/bat"), - (TEST_CONTEXTS, "foo:prefix/value", "http://bar/prefix/value"), - (TEST_CONTEXTS, "value", "value"), - (TEST_CONTEXTS, "v", "http://bar/vocab"), - (TEST_CONTEXTS, "idfoostring", "http://idbar/string"), - (BASE_CONTEXT, "foo", "http://bar/foo"), - (BASE_CONTEXT, "_:foo", "_:foo"), - (BASE_CONTEXT, ":foo", "http://bar/:foo"), - (BASE_CONTEXT, "http:foo", "http:foo"), - (BASE_CONTEXT, ":", "http://bar/:"), - (BASE_CONTEXT, "http://foo/bar", "http://foo/bar"), - ], -) -def test_expand_compact(context, compact_id, expand_id): - ctx = Context(context) - - # Test expansion - assert ctx.expand(compact_id) == expand_id - - # Test compaction - assert ctx.compact(expand_id) == compact_id - - -@pytest.mark.parametrize( - "context,compact_id,expand_id,expand_vocab,vocab", - [ - ( - TEST_CONTEXTS, - "value", - "value", - "http://bar/prefix/value", - "http://bar/vocab", - ), - ( - TEST_CONTEXTS, - "http://foo/bar", - "http://foo/bar", - "http://bar/prefix/http://foo/bar", - "http://bar/vocab", - ), - ], -) -def test_expand_compact_vocab(context, compact_id, expand_id, expand_vocab, vocab): - ctx = Context(context) - - # Test expansion - assert ctx.expand(compact_id) == expand_id - - # Test compaction - assert ctx.compact(expand_id) == compact_id - - # Test vocab expansion - assert ctx.expand_vocab(compact_id, vocab) == expand_vocab - - # Test vocab push - with ctx.vocab_push(vocab): - assert ctx.expand_vocab(compact_id) == expand_vocab - assert ctx.compact_vocab(expand_vocab) == compact_id - # Pushed vocab should not affect regular expansion - assert ctx.expand(compact_id) == expand_id - - assert ctx.expand_vocab(compact_id, vocab) == expand_vocab - assert ctx.compact_vocab(expand_vocab, vocab) == compact_id - - # Vocab with no pushed or specified context is equivalent to base - assert ctx.expand_vocab(compact_id) == expand_id - assert ctx.compact_vocab(expand_id) == compact_id - - -@pytest.mark.parametrize( - "context,compact_id,expand_id,expand_vocab,vocab", + "extra_contexts,compact", [ - (BASE_CONTEXT, "http://bar/foo", "http://bar/foo", None, None), + ([], "nonexist"), + ([], "root"), + ([], "rootPrefix1"), + ([], "rootPrefix2"), + # Test a "Hidden" prefix where the prefix itself doesn't have a + # trailing "/", but it aliases a prefix which does + ([{"h": "rootPrefix2"}], "h:a"), + ([], "rootPrefix2:test"), + ([], "rootPrefix2:test/suffix"), + ([], "rootTerminal1"), + ([], "rootTerminal1:suffix"), + ([], "rootTerminal2"), + ([], "rootTerminal2:suffix"), + ([], "idProperty"), + ([], "vocabProperty"), + ([], "named"), + ([], "named:suffix"), + ([], "named/suffix"), + ([], "_:blank"), + ([], "http:url"), ], ) -def test_expand(context, compact_id, expand_id, expand_vocab, vocab): - """ - This tests expansion edge cases without checking if the compaction will - reverse back - """ - ctx = Context(context) - if expand_vocab is None: - expand_vocab = expand_id - - # Test expansion - assert ctx.expand(compact_id) == expand_id - - # Test vocab expansion - assert ctx.expand_vocab(compact_id, vocab) == expand_vocab - - # Test vocab push - with ctx.vocab_push(vocab): - assert ctx.expand_vocab(compact_id) == expand_vocab - # Pushed vocab should not affect regular expansion - assert ctx.expand(compact_id) == expand_id +def test_expand_compact(extra_contexts, compact): + def test_context(contexts, compact): + ctx = Context(TEST_CONTEXTS + contexts) + root_vocab = ctx.expand_iri("rootVocabProperty") + vocab = ctx.expand_iri("vocabProperty") + + data = { + "@context": TEST_CONTEXTS + contexts, + "@id": compact, + "_:key": { + "@id": "_:id", + compact: "foo", + }, + "_:value": compact, + "rootVocabProperty": compact, + "vocabProperty": compact, + } + + p = subprocess.run( + ["npm", "exec", "--", "jsonld-cli", "expand", "-"], + input=json.dumps(data), + stdout=subprocess.PIPE, + encoding="utf-8", + check=True, + ) + result = json.loads(p.stdout)[0] + + expand_id = result["@id"] + for k in result["_:key"][0].keys(): + if k == "@id": + continue + expand_iri = k + break + else: + expand_iri = compact + + expand_root_vocab = result["http://root/rootVocab"][0]["@id"] + expand_vocab = result["http://root/vocab"][0]["@id"] + + assert result["_:value"][0]["@value"] == compact + + def check(actual, expected, desc): + if actual != expected: + print(json.dumps(data, indent=2)) + assert False, f"{desc} failed: {actual!r} != {expected!r}" + + check(ctx.expand_iri(compact), expand_iri, "Expand IRI") + check(ctx.expand_id(compact), expand_id, "Expand ID") + check(ctx.expand_vocab(compact, vocab), expand_vocab, "Expand vocab") + check(ctx.expand_vocab(compact, None), expand_root_vocab, "Expand no vocab") + check( + ctx.expand_vocab(compact, root_vocab), + expand_root_vocab, + "Expand root vocab", + ) + + check(ctx.compact_iri(expand_iri), compact, "Compact IRI") + check(ctx.compact_id(expand_id), compact, "Compact ID") + check(ctx.compact_vocab(expand_vocab, vocab), compact, "Compact vocab") + check(ctx.compact_vocab(expand_root_vocab, None), compact, "Compact no vocab") + check( + ctx.compact_vocab(expand_root_vocab, root_vocab), + compact, + "Compact root vocab", + ) + + test_context(extra_contexts, compact) + test_context(BASE_CONTEXT + extra_contexts, compact) diff --git a/tests/test_jsonschema.py b/tests/test_jsonschema.py index 08a35bf2..af85006f 100644 --- a/tests/test_jsonschema.py +++ b/tests/test_jsonschema.py @@ -848,7 +848,7 @@ def lst(v): "_:blanknode", "http://serialize.example.org/test", # Named individual - "test-class/named", + "named", ], bad=[ {"@type": "test-another-class"}, From 777b35453e1d6c2f9f66a63dab6368eb83c897ba Mon Sep 17 00:00:00 2001 From: Joshua Watt Date: Fri, 2 Aug 2024 09:15:12 -0600 Subject: [PATCH 12/14] Bump version for release --- src/shacl2code/version.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/shacl2code/version.py b/src/shacl2code/version.py index 83c8ee31..9bba7790 100644 --- a/src/shacl2code/version.py +++ b/src/shacl2code/version.py @@ -1 +1 @@ -VERSION = "0.0.12" +VERSION = "0.0.13" From ae7ba4e7a31dc302b87d971ca32e253fa0dae786 Mon Sep 17 00:00:00 2001 From: Joshua Watt Date: Fri, 2 Aug 2024 10:17:10 -0600 Subject: [PATCH 13/14] Remove pytest-server-fixtures The upstream for this module appears dead, and it is broken with the latest version of python due to [1], so remove it and implement the functionality locally [1]: https://github.com/man-group/pytest-plugins/issues/224 --- pyproject.toml | 1 - tests/__init__.py | 0 tests/conftest.py | 9 +++-- tests/http.py | 74 ++++++++++++++++++++++++++++++++++++++ tests/test_model_source.py | 9 ++--- 5 files changed, 80 insertions(+), 13 deletions(-) create mode 100644 tests/__init__.py create mode 100644 tests/http.py diff --git a/pyproject.toml b/pyproject.toml index d1c45a40..45c4512b 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -31,7 +31,6 @@ dev = [ "pyshacl >= 0.25.0", "pytest >= 7.4", "pytest-cov >= 4.1", - "pytest-server-fixtures >= 1.7", ] [project.urls] diff --git a/tests/__init__.py b/tests/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/tests/conftest.py b/tests/conftest.py index b76561e4..5af07856 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -9,8 +9,8 @@ import shutil import subprocess import time +from .http import HTTPTestServer -from pytest_server_fixtures.http import SimpleHTTPTestServer from pathlib import Path THIS_FILE = Path(__file__) @@ -21,19 +21,18 @@ @pytest.fixture def http_server(): - with SimpleHTTPTestServer() as s: + with HTTPTestServer() as s: s.start() yield s @pytest.fixture(scope="session") def model_server(): - with SimpleHTTPTestServer() as s: - root = Path(s.document_root) + with HTTPTestServer() as s: for p in MODEL_DIR.iterdir(): if not p.is_file(): continue - shutil.copyfile(p, root / p.name) + shutil.copyfile(p, s.document_root / p.name) s.start() yield s.uri diff --git a/tests/http.py b/tests/http.py new file mode 100644 index 00000000..25d932c7 --- /dev/null +++ b/tests/http.py @@ -0,0 +1,74 @@ +# +# Copyright (c) 2024 Joshua Watt +# +# SPDX-License-Identifier: MIT + +import socket +import subprocess +import sys +import tempfile +import time + +from pathlib import Path +from contextlib import closing + + +def get_ephemeral_port(host): + with closing(socket.socket(socket.AF_INET, socket.SOCK_STREAM)) as s: + s.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1) + s.bind((host, 0)) + return s.getsockname()[1] + + +class HTTPTestServer(object): + def __init__(self): + self.p = None + self.temp_dir = None + + def start(self): + assert self.p is None, "Server already started" + + self.host = "127.0.0.1" + self.port = get_ephemeral_port(self.host) + self.p = subprocess.Popen( + [sys.executable, "-m", "http.server", "--bind", self.host, str(self.port)], + cwd=self.document_root, + ) + self.uri = f"http://{self.host}:{self.port}" + + # Wait for server to start + start_time = time.monotonic() + while time.monotonic() < start_time + 30: + assert self.p.poll() is None + with closing(socket.socket(socket.AF_INET, socket.SOCK_STREAM)) as s: + try: + s.connect((self.host, self.port)) + return + + except ConnectionRefusedError: + continue + + # Timeout + self.p.terminate() + self.p.wait() + assert False, "Timeout waiting for server to be ready" + + def stop(self): + if self.p is None: + return + + self.p.terminate() + self.p.wait() + + def __enter__(self): + self.temp_dir = tempfile.TemporaryDirectory() + return self + + def __exit__(self, typ, value, tb): + self.stop() + self.temp_dir.cleanup() + self.temp_dir = None + + @property + def document_root(self): + return Path(self.temp_dir.name) diff --git a/tests/test_model_source.py b/tests/test_model_source.py index 661f942b..379c606c 100644 --- a/tests/test_model_source.py +++ b/tests/test_model_source.py @@ -3,7 +3,6 @@ # # SPDX-License-Identifier: MIT -import os import shutil import subprocess import pytest @@ -289,12 +288,8 @@ def test_context_url(model_server): def test_context_args(http_server): - shutil.copyfile( - TEST_CONTEXT, os.path.join(http_server.document_root, "context.json") - ) - shutil.copyfile( - TEST_CONTEXT, os.path.join(http_server.document_root, "context2.json") - ) + shutil.copyfile(TEST_CONTEXT, http_server.document_root / "context.json") + shutil.copyfile(TEST_CONTEXT, http_server.document_root / "context2.json") def do_test(*, contexts=[], url_contexts=[]): cmd = [ From 8c53ed7984792126d3cbb4f802080abaf1f014be Mon Sep 17 00:00:00 2001 From: Keith Zantow Date: Fri, 2 Aug 2024 16:11:18 -0400 Subject: [PATCH 14/14] feat: initial golang support Co-authored-by: Nisha Kumar Signed-off-by: Keith Zantow --- src/shacl2code/lang/__init__.py | 1 + src/shacl2code/lang/common.py | 3 +- .../lang/go_runtime/graph_builder.go | 314 +++++++++++ src/shacl2code/lang/go_runtime/ld_context.go | 525 ++++++++++++++++++ .../lang/go_runtime/runtime_test.go | 325 +++++++++++ .../lang/go_runtime/superclass_view.go | 62 +++ src/shacl2code/lang/golang.py | 321 +++++++++++ src/shacl2code/lang/templates/golang.j2 | 151 +++++ 8 files changed, 1701 insertions(+), 1 deletion(-) create mode 100644 src/shacl2code/lang/go_runtime/graph_builder.go create mode 100644 src/shacl2code/lang/go_runtime/ld_context.go create mode 100644 src/shacl2code/lang/go_runtime/runtime_test.go create mode 100644 src/shacl2code/lang/go_runtime/superclass_view.go create mode 100644 src/shacl2code/lang/golang.py create mode 100644 src/shacl2code/lang/templates/golang.j2 diff --git a/src/shacl2code/lang/__init__.py b/src/shacl2code/lang/__init__.py index 20d916e3..7589513a 100644 --- a/src/shacl2code/lang/__init__.py +++ b/src/shacl2code/lang/__init__.py @@ -9,3 +9,4 @@ from .jinja import JinjaRender # noqa: F401 from .python import PythonRender # noqa: F401 from .jsonschema import JsonSchemaRender # noqa: F401 +from .golang import GolangRender # noqa: F401 diff --git a/src/shacl2code/lang/common.py b/src/shacl2code/lang/common.py index eb9a1ccb..c022dc00 100644 --- a/src/shacl2code/lang/common.py +++ b/src/shacl2code/lang/common.py @@ -72,10 +72,10 @@ def abort_helper(msg): env.globals["abort"] = abort_helper env.globals["SHACL2CODE"] = SHACL2CODE env.globals["SH"] = SH + template = env.get_template(template.name) render = template.render( - disclaimer=f"This file was automatically generated by {os.path.basename(sys.argv[0])}. DO NOT MANUALLY MODIFY IT", **render_args, ) @@ -135,6 +135,7 @@ def get_all_named_individuals(cls): "abstract_classes": abstract_classes, "enums": enums, "context": model.context, + "disclaimer": f"This file was automatically generated by {os.path.basename(sys.argv[0])}. DO NOT MANUALLY MODIFY IT", **self.get_additional_render_args(), } diff --git a/src/shacl2code/lang/go_runtime/graph_builder.go b/src/shacl2code/lang/go_runtime/graph_builder.go new file mode 100644 index 00000000..273d5468 --- /dev/null +++ b/src/shacl2code/lang/go_runtime/graph_builder.go @@ -0,0 +1,314 @@ +package runtime + +import ( + "fmt" + "reflect" + "strings" +) + +type graphBuilder struct { + ldc ldContext + input []any + graph []any + idPrefix string + nextID map[reflect.Type]int + ids map[reflect.Value]string +} + +func (b *graphBuilder) toGraph() []any { + return b.graph +} + +func (b *graphBuilder) add(o any) (context *serializationContext, err error) { + v := reflect.ValueOf(o) + if v.Type().Kind() != reflect.Pointer { + if v.CanAddr() { + v = v.Addr() + } else { + newV := reflect.New(v.Type()) + newV.Elem().Set(v) + v = newV + } + } + val, err := b.toValue(v) + // objects with IDs get added to the graph during object traversal + if _, isTopLevel := val.(map[string]any); isTopLevel && err == nil { + b.graph = append(b.graph, val) + } + ctx := b.findContext(v.Type()) + return ctx, err +} + +func (b *graphBuilder) findContext(t reflect.Type) *serializationContext { + t = baseType(t) // object may be a pointer, but we want the base types + for _, context := range b.ldc { + for _, typ := range context.iriToType { + if t == typ.typ { + return context + } + } + } + return nil +} + +func (b *graphBuilder) toStructMap(v reflect.Value) (value any, err error) { + t := v.Type() + if t.Kind() != reflect.Struct { + return nil, fmt.Errorf("expected struct type, got: %v", stringify(v)) + } + + meta, ok := fieldByType[ldType](t) + if !ok { + return nil, fmt.Errorf("struct does not have LDType metadata: %v", stringify(v)) + } + + iri := meta.Tag.Get(typeIriCompactTag) + if iri == "" { + iri = meta.Tag.Get(typeIriTag) + } + + context := b.findContext(t) + tc := context.typeToContext[t] + + typeProp := ldTypeProp + if context.typeAlias != "" { + typeProp = context.typeAlias + } + out := map[string]any{ + typeProp: iri, + } + + hasValues := false + id := "" + + for i := 0; i < t.NumField(); i++ { + f := t.Field(i) + if skipField(f) { + continue + } + + prop := f.Tag.Get(propIriCompactTag) + if prop == "" { + prop = f.Tag.Get(propIriTag) + } + + fieldV := v.Field(i) + + if !isRequired(f) && isEmpty(fieldV) { + continue + } + + val, err := b.toValue(fieldV) + if err != nil { + return nil, err + } + + if isIdField(f) { + id, _ = val.(string) + if id == "" { + // if this struct does not have an ID set, and does not have multiple references, + // it is output inline, it does not need an ID, but does need an ID + // when it is moved to the top-level graph and referenced elsewhere + if !b.hasMultipleReferences(v.Addr()) { + continue + } + val, _ = b.ensureID(v.Addr()) + } else if tc != nil { + // compact named IRIs + if _, ok := tc.iriToName[id]; ok { + id = tc.iriToName[id] + } + } + } else { + hasValues = true + } + + out[prop] = val + } + + if id != "" && !hasValues { + // if we _only_ have an ID set and no other values, consider this a named individual + return id, nil + } + + return out, nil +} + +func isIdField(f reflect.StructField) bool { + return f.Tag.Get(propIriTag) == ldIDProp +} + +func isEmpty(v reflect.Value) bool { + return !v.IsValid() || v.IsZero() +} + +func isRequired(f reflect.StructField) bool { + if isIdField(f) { + return true + } + required := f.Tag.Get(propIsRequiredTag) + return required != "" && !strings.EqualFold(required, "false") +} + +func (b *graphBuilder) toValue(v reflect.Value) (any, error) { + if !v.IsValid() { + return nil, nil + } + + switch v.Type().Kind() { + case reflect.Interface: + return b.toValue(v.Elem()) + case reflect.Pointer: + if v.IsNil() { + return nil, nil + } + if !b.hasMultipleReferences(v) { + return b.toValue(v.Elem()) + } + return b.ensureID(v) + case reflect.Struct: + return b.toStructMap(v) + case reflect.Slice: + var out []any + for i := 0; i < v.Len(); i++ { + val, err := b.toValue(v.Index(i)) + if err != nil { + return nil, err + } + out = append(out, val) + } + return out, nil + case reflect.String: + return v.String(), nil + default: + if v.CanInterface() { + return v.Interface(), nil + } + return nil, fmt.Errorf("unable to convert value to maps: %v", stringify(v)) + } +} + +func (b *graphBuilder) ensureID(ptr reflect.Value) (string, error) { + if ptr.Type().Kind() != reflect.Pointer { + return "", fmt.Errorf("expected pointer, got: %v", stringify(ptr)) + } + if id, ok := b.ids[ptr]; ok { + return id, nil + } + + v := ptr.Elem() + t := v.Type() + + id, err := b.getID(v) + if err != nil { + return "", err + } + if id == "" { + if b.nextID == nil { + b.nextID = map[reflect.Type]int{} + } + nextID := b.nextID[t] + 1 + b.nextID[t] = nextID + id = fmt.Sprintf("_:%s-%v", t.Name(), nextID) + } + b.ids[ptr] = id + val, err := b.toValue(v) + if err != nil { + return "", err + } + b.graph = append(b.graph, val) + return id, nil +} + +func (b *graphBuilder) getID(v reflect.Value) (string, error) { + t := v.Type() + if t.Kind() != reflect.Struct { + return "", fmt.Errorf("expected struct, got: %v", stringify(v)) + } + for i := 0; i < t.NumField(); i++ { + f := t.Field(i) + if isIdField(f) { + fv := v.Field(i) + if f.Type.Kind() != reflect.String { + return "", fmt.Errorf("invalid type for ID field %v in: %v", f, stringify(v)) + } + return fv.String(), nil + } + } + return "", nil +} + +// hasMultipleReferences returns true if the ptr value has multiple references in the input slice +func (b *graphBuilder) hasMultipleReferences(ptr reflect.Value) bool { + if !ptr.IsValid() { + return false + } + count := 0 + visited := map[reflect.Value]struct{}{} + for _, v := range b.input { + count += refCountR(ptr, visited, reflect.ValueOf(v)) + if count > 1 { + return true + } + } + return false +} + +// refCount returns the reference count of the value in the container object +func refCount(find any, container any) int { + visited := map[reflect.Value]struct{}{} + ptrV := reflect.ValueOf(find) + if !ptrV.IsValid() { + return 0 + } + return refCountR(ptrV, visited, reflect.ValueOf(container)) +} + +// refCountR recursively searches for the value, find, in the value v +func refCountR(find reflect.Value, visited map[reflect.Value]struct{}, v reflect.Value) int { + if find.Equal(v) { + return 1 + } + if !v.IsValid() { + return 0 + } + if _, ok := visited[v]; ok { + return 0 + } + visited[v] = struct{}{} + switch v.Kind() { + case reflect.Interface: + return refCountR(find, visited, v.Elem()) + case reflect.Pointer: + if v.IsNil() { + return 0 + } + return refCountR(find, visited, v.Elem()) + case reflect.Struct: + count := 0 + for i := 0; i < v.NumField(); i++ { + count += refCountR(find, visited, v.Field(i)) + } + return count + case reflect.Slice: + count := 0 + for i := 0; i < v.Len(); i++ { + count += refCountR(find, visited, v.Index(i)) + } + return count + default: + return 0 + } +} + +func stringify(o any) string { + if v, ok := o.(reflect.Value); ok { + if !v.IsValid() { + return "invalid value" + } + if !v.IsZero() && v.CanInterface() { + o = v.Interface() + } + } + return fmt.Sprintf("%#v", o) +} diff --git a/src/shacl2code/lang/go_runtime/ld_context.go b/src/shacl2code/lang/go_runtime/ld_context.go new file mode 100644 index 00000000..c3fa721c --- /dev/null +++ b/src/shacl2code/lang/go_runtime/ld_context.go @@ -0,0 +1,525 @@ +package runtime + +import ( + "encoding/json" + "errors" + "fmt" + "io" + "path" + "reflect" + "strings" +) + +// ldType is a 0-size data holder property type for type-level linked data +type ldType struct{} + +// ldContext is the holder for all known LD contexts and required definitions +type ldContext map[string]*serializationContext + +// RegisterTypes registers types to be used when serializing/deserialising documents +func (c ldContext) RegisterTypes(contextUrl string, types ...any) ldContext { + ctx := c[contextUrl] + if ctx == nil { + ctx = &serializationContext{ + contextUrl: contextUrl, + typeAlias: "type", // FIXME this needs to come from the LD context + iriToType: map[string]*typeContext{}, + typeToContext: map[reflect.Type]*typeContext{}, + } + c[contextUrl] = ctx + } + for _, typ := range types { + ctx.registerType(typ) + } + return c +} + +// IRIMap registers compact IRIs for the given type +func (c ldContext) IRIMap(contextUrl string, typ any, nameMap map[string]string) ldContext { + c.RegisterTypes(contextUrl) // ensure there is a context created + ctx := c[contextUrl] + + t := reflect.TypeOf(typ) + t = baseType(t) // types should be passed as pointers; we want the base types + tc := ctx.typeToContext[t] + if tc == nil { + ctx.registerType(typ) + tc = ctx.typeToContext[t] + } + for iri, compact := range nameMap { + tc.iriToName[iri] = compact + tc.nameToIri[compact] = iri + } + return c +} + +func (c ldContext) ToJSON(writer io.Writer, value any) error { + vals, err := c.toMaps(value) + if err != nil { + return err + } + enc := json.NewEncoder(writer) + enc.SetEscapeHTML(false) + return enc.Encode(vals) +} + +func (c ldContext) toMaps(o ...any) (values map[string]any, errors error) { + // the ld graph is referenced here + // traverse the go objects to output to the graph + builder := graphBuilder{ + ldc: c, + input: o, + ids: map[reflect.Value]string{}, + } + + var err error + var context *serializationContext + for _, o := range builder.input { + context, err = builder.add(o) + if err != nil { + return nil, err + } + } + + return map[string]any{ + ldContextProp: context.contextUrl, + ldGraphProp: builder.toGraph(), + }, nil +} + +func (c ldContext) FromJSON(reader io.Reader) ([]any, error) { + vals := map[string]any{} + dec := json.NewDecoder(reader) + err := dec.Decode(&vals) + if err != nil { + return nil, err + } + return c.FromMaps(vals) +} + +func (c ldContext) FromMaps(values map[string]any) ([]any, error) { + instances := map[string]reflect.Value{} + + var errs error + var graph []any + + context, _ := values[ldContextProp].(string) + currentContext := c[context] + if currentContext == nil { + return nil, fmt.Errorf("unknown document %s type: %v", ldContextProp, context) + } + + nodes, _ := values[ldGraphProp].([]any) + if nodes == nil { + return nil, fmt.Errorf("%s array not present in root object", ldGraphProp) + } + + // one pass to create all the instances + for _, node := range nodes { + _, err := c.getOrCreateInstance(currentContext, instances, anyType, node) + errs = appendErr(errs, err) + } + + // second pass to fill in all refs + for _, node := range nodes { + got, err := c.getOrCreateInstance(currentContext, instances, anyType, node) + errs = appendErr(errs, err) + if err == nil && got.IsValid() { + graph = append(graph, got.Interface()) + } + } + + return graph, errs +} + +func (c ldContext) getOrCreateInstance(currentContext *serializationContext, instances map[string]reflect.Value, expectedType reflect.Type, incoming any) (reflect.Value, error) { + if isPrimitive(expectedType) { + if convertedVal := convertTo(incoming, expectedType); convertedVal != emptyValue { + return convertedVal, nil + } + return emptyValue, fmt.Errorf("unable to convert incoming value to type %v: %+v", typeName(expectedType), incoming) + } + switch incoming := incoming.(type) { + case string: + instance := c.findById(currentContext, instances, incoming) + if instance != emptyValue { + return instance, nil + } + // not found: have a complex type with string indicates an IRI or other primitive + switch expectedType.Kind() { + case reflect.Pointer: + expectedType = expectedType.Elem() + if isPrimitive(expectedType) { + val, err := c.getOrCreateInstance(currentContext, instances, expectedType, incoming) + if err != nil { + return emptyValue, err + } + instance = reflect.New(expectedType) + instance.Elem().Set(val) + return instance, nil + } + if expectedType.Kind() == reflect.Struct { + return emptyValue, fmt.Errorf("unexpected pointer reference external IRI reference: %v", incoming) + } + fallthrough + case reflect.Struct: + instance = reflect.New(expectedType) + instance = instance.Elem() + err := c.setStructProps(currentContext, instances, instance, map[string]any{ + ldIDProp: incoming, + }) + return instance, err + case reflect.Interface: + return emptyValue, fmt.Errorf("unable to determine appropriate type for external IRI reference: %v", incoming) + default: + } + case map[string]any: + return c.getOrCreateFromMap(currentContext, instances, incoming) + } + return emptyValue, fmt.Errorf("unexpected data type: %#v", incoming) +} + +func convertTo(incoming any, typ reflect.Type) reflect.Value { + v := reflect.ValueOf(incoming) + if v.CanConvert(typ) { + return v.Convert(typ) + } + return emptyValue +} + +func (c ldContext) findById(_ *serializationContext, instances map[string]reflect.Value, incoming string) reflect.Value { + inst, ok := instances[incoming] + if ok { + return inst + } + return emptyValue +} + +func (c ldContext) getOrCreateFromMap(currentContext *serializationContext, instances map[string]reflect.Value, incoming map[string]any) (reflect.Value, error) { + typ, ok := incoming[ldTypeProp].(string) + if !ok && currentContext.typeAlias != "" { + typ, ok = incoming[currentContext.typeAlias].(string) + } + if !ok { + return emptyValue, fmt.Errorf("not a string") + } + + t, ok := currentContext.iriToType[typ] + if !ok { + return emptyValue, fmt.Errorf("don't have type: %v", typ) + } + + id, _ := incoming[ldIDProp].(string) + if id == "" && t.idProp != "" { + id, _ = incoming[t.idProp].(string) + } + inst, ok := instances[id] + if !ok { + inst = reflect.New(baseType(t.typ)) // New(T) returns *T + if id != "" { + // only set instance references when an ID is provided + instances[id] = inst + } + } + + // valid type, make a new one and fill it from the incoming maps + return inst, c.fill(currentContext, instances, inst, incoming) +} + +func (c ldContext) fill(currentContext *serializationContext, instances map[string]reflect.Value, instance reflect.Value, incoming any) error { + switch incoming := incoming.(type) { + case string: + inst := c.findById(currentContext, instances, incoming) + if inst != emptyValue { + return c.setValue(currentContext, instances, instance, inst) + } + // should be an incoming ID if string + return c.setValue(currentContext, instances, instance, map[string]any{ + ldIDProp: incoming, + }) + case map[string]any: + return c.setStructProps(currentContext, instances, instance, incoming) + } + return fmt.Errorf("unsupported incoming data type: %#v attempting to set instance: %#v", incoming, instance.Interface()) +} + +func (c ldContext) setValue(currentContext *serializationContext, instances map[string]reflect.Value, target reflect.Value, incoming any) error { + var errs error + typ := target.Type() + switch typ.Kind() { + case reflect.Slice: + switch incoming := incoming.(type) { + case []any: + return c.setSliceValue(currentContext, instances, target, incoming) + } + // try mapping a single value to an incoming slice + return c.setValue(currentContext, instances, target, []any{incoming}) + case reflect.Struct: + switch incoming := incoming.(type) { + case map[string]any: + return c.setStructProps(currentContext, instances, target, incoming) + case string: + // named individuals just need an object with the iri set + return c.setStructProps(currentContext, instances, target, map[string]any{ + ldIDProp: incoming, + }) + } + case reflect.Interface, reflect.Pointer: + switch incoming := incoming.(type) { + case string, map[string]any: + inst, err := c.getOrCreateInstance(currentContext, instances, typ, incoming) + errs = appendErr(errs, err) + if inst != emptyValue { + target.Set(inst) + return nil + } + } + default: + if newVal := convertTo(incoming, typ); newVal != emptyValue { + target.Set(newVal) + } else { + errs = appendErr(errs, fmt.Errorf("unable to convert %#v to %s, dropping", incoming, typeName(typ))) + } + } + return nil +} + +func (c ldContext) setStructProps(currentContext *serializationContext, instances map[string]reflect.Value, instance reflect.Value, incoming map[string]any) error { + var errs error + typ := instance.Type() + for typ.Kind() == reflect.Pointer { + instance = instance.Elem() + typ = instance.Type() + } + if typ.Kind() != reflect.Struct { + return fmt.Errorf("unable to set struct properties on non-struct type: %#v", instance.Interface()) + } + tc := currentContext.typeToContext[typ] + for i := 0; i < typ.NumField(); i++ { + field := typ.Field(i) + if skipField(field) { + continue + } + fieldVal := instance.Field(i) + + propIRI := field.Tag.Get(propIriTag) + if propIRI == "" { + continue + } + incomingVal, ok := incoming[propIRI] + if !ok { + compactIRI := field.Tag.Get(propIriCompactTag) + if compactIRI != "" { + incomingVal, ok = incoming[compactIRI] + } + } + if !ok { + continue + } + // don't set blank node IDs, these will be regenerated on output + if propIRI == ldIDProp { + if tc != nil { + if str, ok := incomingVal.(string); ok { + if fullIRI, ok := tc.nameToIri[str]; ok { + incomingVal = fullIRI + } + } + } + if isBlankNodeID(incomingVal) { + continue + } + } + errs = appendErr(errs, c.setValue(currentContext, instances, fieldVal, incomingVal)) + } + return errs +} + +func (c ldContext) setSliceValue(currentContext *serializationContext, instances map[string]reflect.Value, target reflect.Value, incoming []any) error { + var errs error + sliceType := target.Type() + if sliceType.Kind() != reflect.Slice { + return fmt.Errorf("expected slice, got: %#v", target) + } + sz := len(incoming) + if sz > 0 { + elemType := sliceType.Elem() + newSlice := reflect.MakeSlice(sliceType, 0, sz) + for i := 0; i < sz; i++ { + incomingValue := incoming[i] + if incomingValue == nil { + continue // don't allow null values + } + newItemValue, err := c.getOrCreateInstance(currentContext, instances, elemType, incomingValue) + errs = appendErr(errs, err) + if newItemValue != emptyValue { + // validate we can actually set the type + if newItemValue.Type().AssignableTo(elemType) { + newSlice = reflect.Append(newSlice, newItemValue) + } + } + } + target.Set(newSlice) + } + return errs +} + +func skipField(field reflect.StructField) bool { + return field.Type.Size() == 0 +} + +func typeName(t reflect.Type) string { + switch { + case isPointer(t): + return "*" + typeName(t.Elem()) + case isSlice(t): + return "[]" + typeName(t.Elem()) + case isMap(t): + return "map[" + typeName(t.Key()) + "]" + typeName(t.Elem()) + case isPrimitive(t): + return t.Name() + } + return path.Base(t.PkgPath()) + "." + t.Name() +} + +func isSlice(t reflect.Type) bool { + return t.Kind() == reflect.Slice +} + +func isMap(t reflect.Type) bool { + return t.Kind() == reflect.Map +} + +func isPointer(t reflect.Type) bool { + return t.Kind() == reflect.Pointer +} + +func isPrimitive(t reflect.Type) bool { + switch t.Kind() { + case reflect.String, + reflect.Int, + reflect.Int8, + reflect.Int16, + reflect.Int32, + reflect.Int64, + reflect.Uint, + reflect.Uint8, + reflect.Uint16, + reflect.Uint32, + reflect.Uint64, + reflect.Float32, + reflect.Float64, + reflect.Bool: + return true + default: + return false + } +} + +const ( + ldIDProp = "@id" + ldTypeProp = "@type" + ldContextProp = "@context" + ldGraphProp = "@graph" + typeIriTag = "iri" + typeIriCompactTag = "iri-compact" + propIriTag = "iri" + propIriCompactTag = "iri-compact" + typeIdPropTag = "id-prop" + propIsRequiredTag = "required" +) + +var ( + emptyValue reflect.Value + anyType = reflect.TypeOf((*any)(nil)).Elem() +) + +type typeContext struct { + typ reflect.Type + iri string + compact string + idProp string + iriToName map[string]string + nameToIri map[string]string +} + +type serializationContext struct { + contextUrl string + typeAlias string + iriToType map[string]*typeContext + typeToContext map[reflect.Type]*typeContext +} + +func fieldByType[T any](t reflect.Type) (reflect.StructField, bool) { + var v T + typ := reflect.TypeOf(v) + for i := 0; i < t.NumField(); i++ { + f := t.Field(i) + if f.Type == typ { + return f, true + } + } + return reflect.StructField{}, false +} + +func (m *serializationContext) registerType(instancePointer any) { + t := reflect.TypeOf(instancePointer) + t = baseType(t) // types should be passed as pointers; we want the base types + + tc := m.typeToContext[t] + if tc != nil { + return // already registered + } + tc = &typeContext{ + typ: t, + iriToName: map[string]string{}, + nameToIri: map[string]string{}, + } + meta, ok := fieldByType[ldType](t) + if ok { + tc.iri = meta.Tag.Get(typeIriTag) + tc.compact = meta.Tag.Get(typeIriCompactTag) + tc.idProp = meta.Tag.Get(typeIdPropTag) + } + for i := 0; i < t.NumField(); i++ { + f := t.Field(i) + if !isIdField(f) { + continue + } + compactIdProp := f.Tag.Get(typeIriCompactTag) + if compactIdProp != "" { + tc.idProp = compactIdProp + } + } + m.iriToType[tc.iri] = tc + m.iriToType[tc.compact] = tc + m.typeToContext[t] = tc +} + +// appendErr appends errors, flattening joined errors +func appendErr(err error, errs ...error) error { + if joined, ok := err.(interface{ Unwrap() []error }); ok { + return errors.Join(append(joined.Unwrap(), errs...)...) + } + if err == nil { + return errors.Join(errs...) + } + return errors.Join(append([]error{err}, errs...)...) +} + +// baseType returns the base type if this is a pointer or interface +func baseType(t reflect.Type) reflect.Type { + switch t.Kind() { + case reflect.Pointer, reflect.Interface: + return baseType(t.Elem()) + default: + return t + } +} + +// isBlankNodeID indicates this is a blank node ID, e.g. _:CreationInfo-1 +func isBlankNodeID(val any) bool { + if val, ok := val.(string); ok { + return strings.HasPrefix(val, "_:") + } + return false +} diff --git a/src/shacl2code/lang/go_runtime/runtime_test.go b/src/shacl2code/lang/go_runtime/runtime_test.go new file mode 100644 index 00000000..2c832240 --- /dev/null +++ b/src/shacl2code/lang/go_runtime/runtime_test.go @@ -0,0 +1,325 @@ +package runtime + +import ( + "bytes" + "encoding/json" + "fmt" + "strings" + "testing" + + "github.com/pmezard/go-difflib/difflib" +) + +/* + SPDX compatible definitions for this test can be generated using something like: + + .venv/bin/python -m shacl2code generate -i https://spdx.org/rdf/3.0.0/spdx-model.ttl -i https://spdx.org/rdf/3.0.0/spdx-json-serialize-annotations.ttl -x https://spdx.org/rdf/3.0.0/spdx-context.jsonld golang --package runtime --output src/shacl2code/lang/go_runtime/generated_code.go --remap-props element=elements,externalIdentifier=externalIdentifiers --include-runtime false +*/ +func Test_spdxExportImportExport(t *testing.T) { + doc := SpdxDocument{ + SpdxId: "old-id", + DataLicense: nil, + Imports: nil, + NamespaceMap: nil, + } + + doc.SetSpdxId("new-id") + + agent := &SoftwareAgent{ + Name: "some-agent", + Summary: "summary", + } + c := &CreationInfo{ + Comment: "some-comment", + Created: "", + CreatedBy: []IAgent{ + agent, + }, + CreatedUsing: []ITool{ + &Tool{ + ExternalIdentifiers: []IExternalIdentifier{ + &ExternalIdentifier{ + ExternalIdentifierType: ExternalIdentifierType_Cpe23, + Identifier: "cpe23:a:myvendor:my-product:*:*:*:*:*:*:*", + }, + }, + Name: "not-tools-golang", + }, + }, + SpecVersion: "", + } + agent.SetCreationInfo(c) + + // add a package + + pkg1 := &Package{ + Name: "some-package-1", + PackageVersion: "1.2.3", + CreationInfo: c, + } + pkg2 := &Package{ + Name: "some-package-2", + PackageVersion: "2.4.5", + CreationInfo: c, + } + doc.Elements = append(doc.Elements, pkg2) + + file1 := &File{ + Name: "/bin/bash", + CreationInfo: c, + } + doc.Elements = append(doc.Elements, file1) + + // add relationships + + doc.Elements = append(doc.Elements, + &Relationship{ + CreationInfo: c, + From: file1, + RelationshipType: RelationshipType_Contains, + To: []IElement{ + pkg1, + pkg2, + }, + }, + ) + + doc.Elements = append(doc.Elements, + &Relationship{ + CreationInfo: c, + From: pkg1, + RelationshipType: RelationshipType_DependsOn, + To: []IElement{ + pkg2, + }, + }, + ) + + doc.Elements = append(doc.Elements, + &AIPackage{ + CreationInfo: c, + TypeOfModel: []string{"a model"}, + }, + ) + + got := encodeDecodeRecode(t, &doc) + + // some basic verification: + + var pkgs []IPackage + for _, e := range got.GetElements() { + if rel, ok := e.(IRelationship); ok && rel.GetRelationshipType() == RelationshipType_Contains { + if from, ok := rel.GetFrom().(IFile); ok && from.GetName() == "/bin/bash" { + for _, el := range rel.GetTo() { + if pkg, ok := el.(IPackage); ok { + pkgs = append(pkgs, pkg) + } + } + + } + } + } + if len(pkgs) != 2 { + t.Error("wrong packages returned") + } +} + +func Test_stringSlice(t *testing.T) { + p := &AIPackage{ + TypeOfModel: []string{"a model"}, + } + encodeDecodeRecode(t, p) +} + +func Test_profileConformance(t *testing.T) { + doc := &SpdxDocument{ + ProfileConformance: []ProfileIdentifierType{ + ProfileIdentifierType_Software, + }, + } + encodeDecodeRecode(t, doc) +} + +func encodeDecodeRecode[T comparable](t *testing.T, obj T) T { + // serialization: + maps, err := ldGlobal.toMaps(obj) + if err != nil { + t.Fatal(err) + } + + buf := bytes.Buffer{} + enc := json.NewEncoder(&buf) + enc.SetEscapeHTML(false) + enc.SetIndent("", " ") + err = enc.Encode(maps) + if err != nil { + t.Fatal(err) + } + + json1 := buf.String() + fmt.Printf("--------- initial JSON: ----------\n%s\n\n", json1) + + // deserialization: + graph, err := ldGlobal.FromJSON(strings.NewReader(json1)) + var got T + for _, entry := range graph { + if e, ok := entry.(T); ok { + got = e + break + } + } + + var empty T + if got == empty { + t.Fatalf("did not find object in graph, json: %s", json1) + } + + // re-serialize: + maps, err = ldGlobal.toMaps(got) + if err != nil { + t.Fatal(err) + } + buf = bytes.Buffer{} + enc = json.NewEncoder(&buf) + enc.SetEscapeHTML(false) + enc.SetIndent("", " ") + err = enc.Encode(maps) + if err != nil { + t.Fatal(err) + } + json2 := buf.String() + fmt.Printf("--------- reserialized JSON: ----------\n%s\n", json2) + + // compare original to parsed and re-encoded + + diff := difflib.UnifiedDiff{ + A: difflib.SplitLines(json1), + B: difflib.SplitLines(json2), + FromFile: "Original", + ToFile: "Current", + Context: 3, + } + text, _ := difflib.GetUnifiedDiffString(diff) + if text != "" { + t.Fatal(text) + } + + return got +} + +func Test_refCount(t *testing.T) { + type O1 struct { + Name string + } + + type O2 struct { + Name string + O1s []*O1 + } + + o1 := &O1{"o1"} + o2 := &O1{"o2"} + o3 := &O1{"o3"} + o21 := &O2{"o21", []*O1{o1, o1, o2, o3}} + o22 := []*O2{ + {"o22-1", []*O1{o1, o1, o1, o1, o2, o3}}, + {"o22-2", []*O1{o1, o1, o1, o1, o2, o3}}, + {"o22-3", []*O1{o1, o1, o1, o1, o2, o3}}, + } + + type O3 struct { + Name string + Ref []*O3 + } + o31 := &O3{"o31", nil} + o32 := &O3{"o32", []*O3{o31}} + o33 := &O3{"o33", []*O3{o32}} + o31.Ref = []*O3{o33} + o34 := &O3{"o34", []*O3{o31, o32}} + o35 := &O3{"o35", []*O3{o31, o32, o31, o32}} + + type O4 struct { + Name string + Ref any + } + o41 := &O4{"o41", nil} + o42 := &O4{"o42", o41} + + tests := []struct { + name string + checkObj any + checkIn any + expected int + }{ + { + name: "none", + checkObj: o33, + checkIn: o21, + expected: 0, + }, + { + name: "interface", + checkObj: o41, + checkIn: o42, + expected: 1, + }, + { + name: "single", + checkObj: o3, + checkIn: o21, + expected: 1, + }, + { + name: "multiple", + checkObj: o1, + checkIn: o21, + expected: 2, + }, + + { + name: "multiple 2", + checkObj: o1, + checkIn: o22, + expected: 12, + }, + { + name: "circular 1", + checkObj: o31, + checkIn: o31, + expected: 1, + }, + { + name: "circular 2", + checkObj: o32, + checkIn: o31, + expected: 1, + }, + { + name: "circular 3", + checkObj: o33, + checkIn: o31, + expected: 1, + }, + { + name: "circular multiple", + checkObj: o32, + checkIn: o34, + expected: 2, + }, + { + name: "circular multiple 2", + checkObj: o32, + checkIn: o35, + expected: 3, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + cnt := refCount(tt.checkObj, tt.checkIn) + if cnt != tt.expected { + t.Errorf("wrong reference count: %v != %v", tt.expected, cnt) + } + }) + } +} diff --git a/src/shacl2code/lang/go_runtime/superclass_view.go b/src/shacl2code/lang/go_runtime/superclass_view.go new file mode 100644 index 00000000..1d96e019 --- /dev/null +++ b/src/shacl2code/lang/go_runtime/superclass_view.go @@ -0,0 +1,62 @@ +package runtime + +import ( + "fmt" + "reflect" +) + +// SuperclassView is a helper function to emulate some semblance of inheritance, +// while still having simple structs without embedding, it is highly experimental +func SuperclassView[View any](base any) *View { + var view *View + baseValue := reflect.ValueOf(base) + baseType := baseValue.Type() + validateBaseType(baseType) // base must be a pointer, see usage examples + viewType := reflect.TypeOf(view) + validateFieldAlignment(baseType, viewType) // base memory layout must be compatible with view + view = reflect.NewAt(viewType.Elem(), baseValue.UnsafePointer()).Interface().(*View) + return view +} + +func validateBaseType(base reflect.Type) { + if base.Kind() != reflect.Pointer { + panic(fmt.Errorf("invalid base type; must be a pointer")) + } + if base.Elem().Kind() != reflect.Struct { + panic(fmt.Errorf("invalid base type; must be a pointer to a struct")) + } +} + +func validateFieldAlignment(base, view reflect.Type) { + // should be passed either 2 pointers to struct types or 2 struct types + if base.Kind() == reflect.Pointer && view.Kind() == reflect.Pointer { + base = base.Elem() + view = view.Elem() + } + if base.Kind() != reflect.Struct || view.Kind() != reflect.Struct { + panic(fmt.Errorf("base and view types must point to structs; got base: %s and view: %s", typeName(base), typeName(view))) + } + // view needs to be a subset of the number of fields in base + if view.NumField() > base.NumField() { + panic(fmt.Errorf("view %s (%d fields) is not a subset of %s (%d fields)", typeName(view), view.NumField(), typeName(base), base.NumField())) + } + for i := 0; i < view.NumField(); i++ { + baseField := base.Field(i) + viewField := view.Field(i) + // ignore zero-sized fields + if baseField.Type.Size() == 0 && viewField.Type.Size() == 0 { + continue + } + // field layout must be identical, name _should_ be the same + if baseField.Name != viewField.Name { + panic(fmt.Errorf("field %d in base is named %s but view expects %s", i, baseField.Name, viewField.Name)) + } + if baseField.Type != viewField.Type { + panic(fmt.Errorf("field %d in base is has type %s but view expects %s", i, typeName(baseField.Type), typeName(viewField.Type))) + } + if baseField.Offset != viewField.Offset { + panic(fmt.Errorf("field %d in base is named %d but view expects %d", i, baseField.Offset, viewField.Offset)) + } + // seems to align + } +} diff --git a/src/shacl2code/lang/golang.py b/src/shacl2code/lang/golang.py new file mode 100644 index 00000000..417e1c39 --- /dev/null +++ b/src/shacl2code/lang/golang.py @@ -0,0 +1,321 @@ +# SPDX-License-Identifier: MIT + +from .common import BasicJinjaRender +from .lang import language, TEMPLATE_DIR +from ..model import Property + +import os +import sys +import subprocess +import re +import inspect + +DATATYPES = { + "http://www.w3.org/2001/XMLSchema#string": "string", + "http://www.w3.org/2001/XMLSchema#anyURI": "string", + "http://www.w3.org/2001/XMLSchema#integer": "int", + "http://www.w3.org/2001/XMLSchema#positiveInteger": "uint", # "PInt", + "http://www.w3.org/2001/XMLSchema#nonNegativeInteger": "uint", + "http://www.w3.org/2001/XMLSchema#boolean": "bool", + "http://www.w3.org/2001/XMLSchema#decimal": "float64", + "http://www.w3.org/2001/XMLSchema#dateTime": "string", # "DateTime", + "http://www.w3.org/2001/XMLSchema#dateTimeStamp": "string", # "DateTimeStamp", +} + +RESERVED_WORDS = { + "package" +} + + +@language("golang") +class GolangRender(BasicJinjaRender): + HELP = "Go Language Bindings" + # conform to go:generate format: https://pkg.go.dev/cmd/go#hdr-Generate_Go_files_by_processing_source + disclaimer = f"Code generated by {os.path.basename(sys.argv[0])}. DO NOT EDIT." + + # set during render, for convenience + render_args = None + + # arg defaults: + package_name = "model" + license_id = False + use_embedding = False + getter_prefix = "Get" + setter_prefix = "Set" + export_structs = "true" + interface_prefix = "I" + interface_suffix = "" + struct_prefix = "" + struct_suffix = "" + embedded_prefix = "super_" + pluralize = False + pluralize_length = 3 + include_runtime = "true" + include_view_pointers = False + as_concrete_prefix = "As" + uppercase_constants = True + constant_separator = "_" + remap_props = "" + remap_props_map = {} + + @classmethod + def get_arguments(cls, parser): + super().get_arguments(parser) + parser.add_argument("--package-name", help="Go package name to generate", default=cls.package_name) + parser.add_argument("--license-id", help="SPDX License identifier to include in the generated code", default=cls.license_id) + parser.add_argument("--use-embedding", type=bool, help="use embedded structs", default=cls.use_embedding) + parser.add_argument("--export-structs", help="export structs", default=cls.export_structs) + parser.add_argument("--struct-suffix", help="struct stuffix", default=cls.struct_suffix) + parser.add_argument("--interface-prefix", help="interface prefix", default=cls.interface_prefix) + parser.add_argument("--include-runtime", help="include runtime functions inline", default=cls.include_runtime) + parser.add_argument("--include-view-pointers", type=bool, help="include runtime functions inline", default=cls.include_view_pointers) + parser.add_argument("--disclaimer", help="file header", default=cls.disclaimer) + parser.add_argument("--remap-props", help="property name mapping", default=cls.remap_props) + parser.add_argument("--pluralize", default=cls.pluralize) + + def __init__(self, args): + super().__init__(args, TEMPLATE_DIR / "golang.j2") + for k, v in inspect.getmembers(args): + if k in GolangRender.__dict__ and not k in BasicJinjaRender.__dict__: + setattr(self, k, v) + + def render(self, template, output, *, extra_env={}, render_args={}): + if self.remap_props: + self.remap_props_map = dict(item.split("=") for item in self.remap_props.split(",")) + + class FW: + d = "" + + def write(self, d): + self.d += d + + w = FW() + self.render_args = render_args + super().render(template, w, extra_env=extra_env, render_args=render_args) + formatted = gofmt(w.d) + output.write(formatted) + + def get_extra_env(self): + return { + "trim_iri": trim_iri, + "indent": indent, + "upper_first": upper_first, + "lower_first": lower_first, + "is_array": is_array, + "comment": comment, + } + + def get_additional_render_args(self): + render_args = {} + # add all directly defined functions and variables + for k, v in inspect.getmembers(self): + if k.startswith("_") or k in BasicJinjaRender.__dict__: + continue + render_args[k] = v + return render_args + + def parents(self,cls): + return [self.all_classes().get(parent_id) for parent_id in cls.parent_ids] + + def properties(self,cls): + props = cls.properties + if cls.id_property and not cls.parent_ids: + return [ + Property( + path="@id", + datatype="http://www.w3.org/2001/XMLSchema#string", + min_count=1, # is this accurate? + max_count=1, + varname=cls.id_property, + comment="identifier property", + ), + ] + props + return props + + def all_classes(self): + return self.render_args["classes"] + + def pluralize_name(self,str): + if not self.pluralize: + return str + if len(str) < self.pluralize_length: + return str + if not str.endswith('s'): + return str + "s" + return str + + def struct_prop_name(self,prop): + # prop: + # class_id, comment, datatype, enum_values, max_count, min_count, path, pattern, varname + name = prop.varname + if is_array(prop): + name = self.pluralize_name(name) + + if name in self.remap_props_map: + name = self.remap_props_map[name] + + name = type_name(name) + + if self.export_structs.lower() != "false": + return upper_first(name) + + return lower_first(name) + + def prop_type(self,prop): + # prop: + # class_id, comment, datatype, enum_values, max_count, min_count, path, pattern, varname + if prop.datatype in DATATYPES: + typ = DATATYPES[prop.datatype] + else: + cls = self.all_classes().get(prop.class_id) + if self.requires_interface(cls): + typ = self.interface_name(cls) + else: + typ = self.struct_name(cls) + + return typ + + def parent_has_prop(self, cls, prop): + for parent in self.parents(cls): + for p in self.properties(parent): + if p.varname == prop.varname: + return True + if self.parent_has_prop(parent, prop): + return True + + return False + + def requires_interface(self,cls): + if cls.properties: + return True + if cls.derived_ids: + return True + # if cls.named_individuals: + # return False + # if cls.node_kind == rdflib.term.URIRef('http://www.w3.org/ns/shacl#BlankNodeOrIRI'): + # return True + if cls.parent_ids: + return True + return False + + def include_prop(self, cls, prop): + return not self.parent_has_prop(cls, prop) + + def interface_name(self,cls): + return upper_first(self.interface_prefix + type_name(cls.clsname) + self.interface_suffix) + + def struct_name(self,cls): + name = self.struct_prefix + type_name(cls.clsname) + self.struct_suffix + if self.export_structs: + name = upper_first(name) + else: + name = lower_first(name) + + if name in RESERVED_WORDS: + return name + "_" + + return name + + def pretty_name(self,cls): + return upper_first(type_name(cls.clsname)) + + def constant_var_name(self,named): + if self.uppercase_constants: + return upper_first(named.varname) + return named.varname + + def getter_name(self,prop): + return self.getter_prefix + upper_first(self.struct_prop_name(prop)) + + def setter_name(self,prop): + return self.setter_prefix + upper_first(self.struct_prop_name(prop)) + + def concrete_name(self,cls): + if self.export_structs.lower() != "false": + return self.struct_name(cls) + if self.use_embedding: + return self.embedded_prefix + upper_first(self.struct_name(cls)) + if cls.is_abstract: + return self.struct_name(cls) + return upper_first(self.struct_name(cls)) + + def include_runtime_code(self): + if self.include_runtime.lower() == "false": + return "" + + package_replacer = "package[^\n]+" + import_replacer = "import[^)]+\\)" + code = "" + dir = os.path.join(os.path.dirname(os.path.abspath(__file__)), "go_runtime") + with open(os.path.join(dir, "ld_context.go")) as f: + code += re.sub(package_replacer, "", f.read()) + with open(os.path.join(dir, "graph_builder.go")) as f: + code += re.sub(package_replacer, "", re.sub(import_replacer, "", f.read())) + if self.include_view_pointers: + with open(os.path.join(dir, "superclass_view.go")) as f: + code += re.sub(package_replacer, "", re.sub(import_replacer, "", f.read())) + return code + + +# common utility functions + + +def upper_first(str): + return str[0].upper() + str[1:] + + +def lower_first(str): + return str[0].lower() + str[1:] + + +def indent(indent_with, str): + parts = re.split("\n", str) + return indent_with + ("\n"+indent_with).join(parts) + + +def dedent(str, amount): + prefix = ' ' * amount + parts = re.split("\n", str) + for i in range(len(parts)): + if parts[i].startswith(prefix): + parts[i] = parts[i][len(prefix):] + return '\n'.join(parts) + + +def type_name(name): + if isinstance(name, list): + name = "".join(name) + parts = re.split(r'[^a-zA-Z0-9]', name) + part = parts[len(parts)-1] + return upper_first(part) + + +def gofmt(code): + try: + proc = subprocess.Popen(["gofmt"], stdout=subprocess.PIPE, stderr=subprocess.PIPE, stdin=subprocess.PIPE, text=True) + result = proc.communicate(input=code) + if result[0] != "": + return result[0] + return code + except: + return code + + +def is_array(prop): + return prop.max_count is None or prop.max_count > 1 + + +def comment(indent_with, identifier, text): + if text.lower().startswith(identifier.lower()): + text = text[len(identifier):] + text = identifier + " " + lower_first(text) + return indent(indent_with, text) + + +def trim_iri(base,iri): + if not base.endswith("/"): + base += "/" + if False and iri.startswith(base): + return iri[len(base):] + return iri + diff --git a/src/shacl2code/lang/templates/golang.j2 b/src/shacl2code/lang/templates/golang.j2 new file mode 100644 index 00000000..db2a0a5d --- /dev/null +++ b/src/shacl2code/lang/templates/golang.j2 @@ -0,0 +1,151 @@ +// {{ disclaimer }} +{%- if license_id %} +// +// SPDX-License-Identifier: {{ license_id }} +{%- endif %} + +package {{ package_name }} + +{#- include runtime code inline #} +{{- include_runtime_code() }} + +{#- struct_props outputs all properties for concrete struct implementations #} +{%- macro struct_props(cls) %} + {#- there is no embedding, so recursively output all parent properties #} + {%- for parent in parents(cls) %} + {%- if not use_embedding %} + {{- struct_props(parent) }} + {%- else %} + {{ concrete_name(parent) }} + {%- endif %} + {% endfor %} + {#- output direct struct properties #} + {% for prop in properties(cls) %} + {%- if include_prop(cls,prop) %} + {{ comment("// ", struct_prop_name(prop), prop.comment)|indent }} + {{ struct_prop_name(prop) }} {% if is_array(prop) %}[]{% endif %}{{ prop_type(prop) }} `iri:"{{ prop.path }}" iri-compact:"{{ context.compact_vocab(prop.path) }}"` + {%- endif %} + {%- endfor %} +{%- endmacro %} + +{#- struct_funcs outputs all functions for concrete struct implementations #} +{%- macro struct_funcs(base,cls) %} + {#- embedded structs may be expanded props #} + {%- if not use_embedding %} + {%- for parent in parents(cls) %} + {{ struct_funcs(base,parent) }} + {%- endfor %} + {%- endif %} + +{% if requires_interface(cls) and include_view_pointers %} +func (o *{{ struct_name(base) }}) {{ as_concrete_prefix }}{{ concrete_name(cls) }}() *{{ concrete_name(cls) }} { + {%- if base == cls %} + return o + {%- else %} + return SuperclassView[{{ concrete_name(cls) }}](o) + {%- endif %} +} +{%- endif %} + {%- for prop in properties(cls) %} + {%- if include_prop(cls,prop) %} +func (o *{{ struct_name(base) }}) {{ getter_name(prop) }}() {% if is_array(prop) %}[]{% endif %}{{ prop_type(prop) }} { + return o.{{ struct_prop_name(prop) }} +} +func (o *{{ struct_name(base) }}) {{ setter_name(prop) }}(v {% if is_array(prop) %}...{% endif %}{{ prop_type(prop) }}) { + o.{{ struct_prop_name(prop) }} = v +} + {%- endif %} + {%- endfor %} +{%- endmacro %} + +{#- interface_props outputs all interface property definitions #} +{%- macro interface_props(cls) -%} + {#- embedding parent interfaces for proper inheritance #} + {%- for parent in parents(cls) %} + {{- interface_name(parent) }} + {% endfor %} + {%- for prop in properties(cls) %} + {%- if include_prop(cls,prop) %} + {{ comment("// ", getter_name(prop), prop.comment)|indent }} + {{ getter_name(prop) }}() {% if is_array(prop) %}[]{% endif %}{{ prop_type(prop) }} + + {{ setter_name(prop) }}({% if is_array(prop) %}...{% endif %}{{ prop_type(prop) }}) + {% endif %} + {%- endfor %} +{%- endmacro %} + +{#- ------------------------ CONSTRUCTOR ------------------------ #} +{%- macro constructor(cls) %} +func New{{ pretty_name(cls) }}() {{ interface_name(cls) }} { + return &{{ struct_name(cls) }}{} +} +{%- endmacro %} + +{#- ------------------------ INTERFACE ------------------------ #} +{%- macro interface(cls) %} +type {{ interface_name(cls) }} interface { + {{ interface_props(cls) }} +} +{%- endmacro %} + +{#- ------------------------ STRUCT ------------------------ #} +{%- macro struct(cls) %} +type {{ struct_name(cls) }} struct { + _ ldType `iri:"{{ cls._id }}" iri-compact:"{{ context.compact_vocab(cls._id) }}"{% if cls.id_property %} id-prop:"{{ cls.id_property }}"{% endif %}` + {% if not cls.id_property %} + Iri string `iri:"@id"` + {%- endif %} + {{- struct_props(cls) }} +} +{%- endmacro %} + +{#- ------------ CLASSES AND INTERFACES -------------- #} +{%- for cls in classes %} + {#- output the interface definition if required #} + {%- if requires_interface(cls) %} + {{ interface(cls) }} + {%- endif %} + + {#- output the struct definition #} + {{ struct(cls) }} + + {%- if include_view_pointers and concrete_name(cls) != struct_name(cls) %} + type {{ concrete_name(cls) }} = {{ struct_name(cls) }} + {%- endif %} + + {#- output any named constants #} + {%- if cls.named_individuals %} + var ( + {%- for ind in cls.named_individuals %} + {{ pretty_name(cls) }}{{ constant_separator }}{{ constant_var_name(ind) }} {%- if requires_interface(cls) %} {{ interface_name(cls) }} {%- endif %} = {{ struct_name(cls) }}{ {% if not cls.id_property %}Iri{% else %}{{ cls.id_property }}{% endif %}:"{{ trim_iri(cls._id,ind._id) }}" } + {%- endfor %} + ) + {%- endif %} + + {%- if not cls.is_abstract and requires_interface(cls) %} + {{ constructor(cls) }} + {%- endif %} + + {{ struct_funcs(cls,cls) }} +{%- endfor %} + +{#- ------------ type mapping for serialization -------------- #} +var ldGlobal = ldContext{} +{%- for url in context.urls %}. + RegisterTypes("{{ url }}", + {%- for cls in classes %} + &{{ struct_name(cls) }}{}, + {%- endfor %} + ) + {%- for cls in classes %} + {%- for prop in properties(cls) %} + {%- if prop.enum_values -%}. + IRIMap("{{ url }}", &{{ prop_type(prop) }}{}, map[string]string{ + {%- for iri in prop.enum_values %} + "{{ iri }}": "{{ context.compact_vocab(iri, prop.path) }}", + {%- endfor %} + }) + {%- endif %} + {%- endfor %} + {%- endfor %} +{%- endfor %}