diff options
Diffstat (limited to 'test/schema/test_serialize.py')
-rw-r--r-- | test/schema/test_serialize.py | 1030 |
1 files changed, 1030 insertions, 0 deletions
diff --git a/test/schema/test_serialize.py b/test/schema/test_serialize.py new file mode 100644 index 0000000..fc6b20a --- /dev/null +++ b/test/schema/test_serialize.py @@ -0,0 +1,1030 @@ +""" + +Part of the tagit test suite. +A copy of the license is provided with the project. +Author: Matthias Baumgartner, 2022 +""" +# imports +import re +import unittest + +# bsfs imports +from bsfs.namespace import ns +from bsfs.schema import Schema, types +from bsfs.utils import errors, URI + +# objects to test +from bsfs.schema.serialize import from_string, to_string + + +## code ## + +class TestFromString(unittest.TestCase): + + def test_empty(self): + # schema contains at least the root types + self.assertEqual(from_string(''), Schema()) + + + def test_circular_dependency(self): + # must not have circular dependencies + self.assertRaises(errors.ConsistencyError, from_string, ''' + prefix rdfs: <http://www.w3.org/2000/01/rdf-schema#> + prefix bsfs: <http://bsfs.ai/schema/> + bsfs:Entity rdfs:subClassOf bsfs:Node . + # ah, a nice circular dependency + bsfs:Entity rdfs:subClassOf bsfs:Document . + bsfs:Document rdfs:subClassOf bsfs:Entity . + bsfs:PDF rdfs:subClassOf bsfs:Document . + ''') + + + def test_node(self): + # all nodes must be defined + self.assertRaises(errors.ConsistencyError, from_string, ''' + prefix rdfs: <http://www.w3.org/2000/01/rdf-schema#> + prefix xsd: <http://www.w3.org/2001/XMLSchema#> + prefix bsfs: <http://bsfs.ai/schema/> + prefix bse: <http://bsfs.ai/schema/Entity#> + + xsd:string rdfs:subClassOf bsfs:Literal . + + bse:filename rdfs:subClassOf bsfs:Predicate ; + rdfs:domain bsfs:Entity ; + rdfs:range xsd:string ; + bsfs:unique "false"^^xsd:boolean . + ''') + + # node definitions must be consistent (cannot re-use a node uri) + self.assertRaises(errors.ConsistencyError, from_string, ''' + prefix rdfs: <http://www.w3.org/2000/01/rdf-schema#> + prefix xsd: <http://www.w3.org/2001/XMLSchema#> + prefix bsfs: <http://bsfs.ai/schema/> + + bsfs:Entity rdfs:subClassOf bsfs:Node . + bsfs:Document rdfs:subClassOf bsfs:Node . + bsfs:Document rdfs:subClassOf bsfs:Entity . # conflicting parent + ''') + + # additional nodes can be defined + n_unused = types.ROOT_NODE.child(ns.bsfs.unused) + self.assertEqual(Schema({}, {n_unused}), from_string(''' + prefix rdfs: <http://www.w3.org/2000/01/rdf-schema#> + prefix xsd: <http://www.w3.org/2001/XMLSchema#> + prefix bsfs: <http://bsfs.ai/schema/> + prefix bse: <http://bsfs.ai/schema/Entity#> + + bsfs:unused rdfs:subClassOf bsfs:Node . # unused symbol + ''')) + + # a node can have multiple children + n_ent = types.ROOT_NODE.child(ns.bsfs.Entity) + n_tag = types.ROOT_NODE.child(ns.bsfs.Tag) + n_doc = n_ent.child(ns.bsfs.Document) + n_image = n_ent.child(ns.bsfs.Image) + self.assertEqual(Schema({}, {n_ent, n_tag, n_doc, n_image}), from_string(''' + prefix rdfs: <http://www.w3.org/2000/01/rdf-schema#> + prefix xsd: <http://www.w3.org/2001/XMLSchema#> + prefix bsfs: <http://bsfs.ai/schema/> + prefix bse: <http://bsfs.ai/schema/Entity#> + + # nodes inherit from same parent + bsfs:Entity rdfs:subClassOf bsfs:Node . + bsfs:Tag rdfs:subClassOf bsfs:Node . + + # nodes inherit from same parent + bsfs:Document rdfs:subClassOf bsfs:Entity . + bsfs:Image rdfs:subClassOf bsfs:Entity . + ''')) + + # additional nodes can be defined and used + n_ent = types.ROOT_NODE.child(ns.bsfs.Entity) + l_string = types.ROOT_LITERAL.child(ns.xsd.string) + p_filename = types.ROOT_PREDICATE.child(ns.bse.filename, + n_ent, l_string, False) + self.assertEqual(Schema({p_filename}), from_string(''' + prefix rdfs: <http://www.w3.org/2000/01/rdf-schema#> + prefix xsd: <http://www.w3.org/2001/XMLSchema#> + prefix bsfs: <http://bsfs.ai/schema/> + prefix bse: <http://bsfs.ai/schema/Entity#> + + bsfs:Entity rdfs:subClassOf bsfs:Node . + xsd:string rdfs:subClassOf bsfs:Literal . + + bse:filename rdfs:subClassOf bsfs:Predicate ; + rdfs:domain bsfs:Entity ; + rdfs:range xsd:string ; + bsfs:unique "false"^^xsd:boolean . + ''')) + + # nodes can have annotations + self.assertDictEqual(from_string(''' + prefix rdfs: <http://www.w3.org/2000/01/rdf-schema#> + prefix xsd: <http://www.w3.org/2001/XMLSchema#> + prefix bsfs: <http://bsfs.ai/schema/> + + bsfs:Entity rdfs:subClassOf bsfs:Node . + + ''').node(ns.bsfs.Entity).annotations, {}) + self.assertDictEqual(from_string(''' + prefix rdfs: <http://www.w3.org/2000/01/rdf-schema#> + prefix xsd: <http://www.w3.org/2001/XMLSchema#> + prefix bsfs: <http://bsfs.ai/schema/> + + bsfs:Entity rdfs:subClassOf bsfs:Node ; + rdfs:label "hello world"^^xsd:string ; + bsfs:foo "1234"^^xsd:integer . + + ''').node(ns.bsfs.Entity).annotations, { + ns.rdfs.label: 'hello world', + ns.bsfs.foo: 1234, + }) + + + def test_literal(self): + # all literals must be defined + self.assertRaises(errors.ConsistencyError, from_string, ''' + prefix rdfs: <http://www.w3.org/2000/01/rdf-schema#> + prefix xsd: <http://www.w3.org/2001/XMLSchema#> + prefix bsfs: <http://bsfs.ai/schema/> + prefix bse: <http://bsfs.ai/schema/Entity#> + + bsfs:Entity rdfs:subClassOf bsfs:Node . + + bse:filename rdfs:subClassOf bsfs:Predicate ; + rdfs:domain bsfs:Entity ; + rdfs:range xsd:string ; # undefined symbol + bsfs:unique "false"^^xsd:boolean . + ''') + + # literal definitions must be consistent (cannot re-use a literal uri) + self.assertRaises(errors.ConsistencyError, from_string, ''' + prefix rdfs: <http://www.w3.org/2000/01/rdf-schema#> + prefix xsd: <http://www.w3.org/2001/XMLSchema#> + prefix bsfs: <http://bsfs.ai/schema/> + + xsd:string rdfs:subClassOf bsfs:Literal . + xsd:name rdfs:subClassOf bsfs:Literal . + xsd:name rdfs:subClassOf xsd:string . # conflicting parent + ''') + + # additional literals can be defined + l_unused = types.ROOT_LITERAL.child(ns.xsd.unused) + self.assertEqual(Schema({}, {}, {l_unused}), from_string(''' + prefix rdfs: <http://www.w3.org/2000/01/rdf-schema#> + prefix xsd: <http://www.w3.org/2001/XMLSchema#> + prefix bsfs: <http://bsfs.ai/schema/> + prefix bse: <http://bsfs.ai/schema/Entity#> + + xsd:unused rdfs:subClassOf bsfs:Literal . # unused symbol + ''')) + + # a literal can have multiple children + l_string = types.ROOT_LITERAL.child(ns.xsd.string) + l_integer = types.ROOT_NUMBER.child(ns.xsd.integer) + l_unsigned = l_integer.child(ns.xsd.unsigned) + l_signed = l_integer.child(ns.xsd.signed) + self.assertEqual(Schema({}, {}, {l_string, l_integer, l_unsigned, l_signed}), from_string(''' + prefix rdfs: <http://www.w3.org/2000/01/rdf-schema#> + prefix xsd: <http://www.w3.org/2001/XMLSchema#> + prefix bsfs: <http://bsfs.ai/schema/> + prefix bse: <http://bsfs.ai/schema/Entity#> + + # literals inherit from same parent + xsd:string rdfs:subClassOf bsfs:Literal . + bsfs:Number rdfs:subClassOf bsfs:Literal . + xsd:integer rdfs:subClassOf bsfs:Number . + + # literals inherit from same parent + xsd:unsigned rdfs:subClassOf xsd:integer . + xsd:signed rdfs:subClassOf xsd:integer . + ''')) + + # additional literals can be defined and used + n_ent = types.ROOT_NODE.child(ns.bsfs.Entity) + l_string = types.ROOT_LITERAL.child(ns.xsd.string) + p_filename = types.ROOT_PREDICATE.child(ns.bse.filename, + n_ent, l_string, False) + self.assertEqual(Schema({p_filename}), from_string(''' + prefix rdfs: <http://www.w3.org/2000/01/rdf-schema#> + prefix xsd: <http://www.w3.org/2001/XMLSchema#> + prefix bsfs: <http://bsfs.ai/schema/> + prefix bse: <http://bsfs.ai/schema/Entity#> + + bsfs:Entity rdfs:subClassOf bsfs:Node . + xsd:string rdfs:subClassOf bsfs:Literal . + + bse:filename rdfs:subClassOf bsfs:Predicate ; + rdfs:domain bsfs:Entity ; + rdfs:range xsd:string ; + bsfs:unique "false"^^xsd:boolean . + ''')) + + # literals can have annotations + self.assertDictEqual(from_string(''' + prefix rdfs: <http://www.w3.org/2000/01/rdf-schema#> + prefix xsd: <http://www.w3.org/2001/XMLSchema#> + prefix bsfs: <http://bsfs.ai/schema/> + + xsd:string rdfs:subClassOf bsfs:Literal . + + ''').literal(ns.xsd.string).annotations, {}) + self.assertDictEqual(from_string(''' + prefix rdfs: <http://www.w3.org/2000/01/rdf-schema#> + prefix xsd: <http://www.w3.org/2001/XMLSchema#> + prefix bsfs: <http://bsfs.ai/schema/> + + xsd:string rdfs:subClassOf bsfs:Literal ; + rdfs:label "hello world"^^xsd:string ; + bsfs:foo "1234"^^xsd:integer . + + ''').literal(ns.xsd.string).annotations, { + ns.rdfs.label: 'hello world', + ns.bsfs.foo: 1234, + }) + + + def test_predicate(self): + # domain must be defined + self.assertRaises(errors.ConsistencyError, from_string, ''' + prefix rdfs: <http://www.w3.org/2000/01/rdf-schema#> + prefix xsd: <http://www.w3.org/2001/XMLSchema#> + prefix bsfs: <http://bsfs.ai/schema/> + prefix bse: <http://bsfs.ai/schema/Entity#> + + xsd:string rdfs:subClassOf bsfs:Literal . + + bse:filename rdfs:subClassOf bsfs:Predicate ; + rdfs:domain bsfs:Entity ; # undefined symbol + rdfs:range xsd:string ; + bsfs:unique "false"^^xsd:boolean . + ''') + # domain cannot be a literal + self.assertRaises(errors.ConsistencyError, from_string, ''' + prefix rdfs: <http://www.w3.org/2000/01/rdf-schema#> + prefix xsd: <http://www.w3.org/2001/XMLSchema#> + prefix bsfs: <http://bsfs.ai/schema/> + prefix bse: <http://bsfs.ai/schema/Entity#> + + bsfs:Entity rdfs:subClassOf bsfs:Literal . + xsd:string rdfs:subClassOf bsfs:Literal . + + bse:filename rdfs:subClassOf bsfs:Predicate ; + rdfs:domain bsfs:Entity ; # literal instead of node + rdfs:range xsd:string ; + bsfs:unique "false"^^xsd:boolean . + ''') + + # range must be defined + self.assertRaises(errors.ConsistencyError, from_string, ''' + prefix rdfs: <http://www.w3.org/2000/01/rdf-schema#> + prefix xsd: <http://www.w3.org/2001/XMLSchema#> + prefix bsfs: <http://bsfs.ai/schema/> + prefix bse: <http://bsfs.ai/schema/Entity#> + + bsfs:Entity rdfs:subClassOf bsfs:Node . + + bse:filename rdfs:subClassOf bsfs:Predicate ; + rdfs:domain bsfs:Entity ; + rdfs:range xsd:string ; # undefined symbol + bsfs:unique "false"^^xsd:boolean . + ''') + # range must be defined + self.assertRaises(errors.ConsistencyError, from_string, ''' + prefix rdfs: <http://www.w3.org/2000/01/rdf-schema#> + prefix xsd: <http://www.w3.org/2001/XMLSchema#> + prefix bsfs: <http://bsfs.ai/schema/> + prefix bse: <http://bsfs.ai/schema/Entity#> + + bsfs:Entity rdfs:subClassOf bsfs:Node . + + bse:filename rdfs:subClassOf bsfs:Predicate ; + rdfs:domain bsfs:Entity ; + rdfs:range bsfs:Foo ; # undefined symbol + bsfs:unique "false"^^xsd:boolean . + ''') + # range must be a node or a literal + self.assertRaises(errors.ConsistencyError, from_string, ''' + prefix rdfs: <http://www.w3.org/2000/01/rdf-schema#> + prefix xsd: <http://www.w3.org/2001/XMLSchema#> + prefix bsfs: <http://bsfs.ai/schema/> + prefix bse: <http://bsfs.ai/schema/Entity#> + + bsfs:Entity rdfs:subClassOf bsfs:Node . + + bse:filename rdfs:subClassOf bsfs:Predicate ; + rdfs:domain bsfs:Entity ; + rdfs:range bsfs:Predicate ; # invalid symbol + bsfs:unique "false"^^xsd:boolean . + ''') + + # additional predicates can be defined + n_ent = types.ROOT_NODE.child(ns.bsfs.Entity) + l_string = types.ROOT_LITERAL.child(ns.xsd.string) + p_comment = types.ROOT_PREDICATE.child(ns.bse.comment, domain=n_ent, range=l_string, unique=False) + self.assertEqual(Schema({p_comment}), from_string(''' + prefix rdfs: <http://www.w3.org/2000/01/rdf-schema#> + prefix xsd: <http://www.w3.org/2001/XMLSchema#> + prefix bsfs: <http://bsfs.ai/schema/> + prefix bse: <http://bsfs.ai/schema/Entity#> + + bsfs:Entity rdfs:subClassOf bsfs:Node . + xsd:string rdfs:subClassOf bsfs:Literal . + + bse:comment rdfs:subClassOf bsfs:Predicate ; + rdfs:domain bsfs:Entity ; + rdfs:range xsd:string ; + bsfs:unique "false"^^xsd:boolean . + ''')) + + # predicates inherit properties from parents + n_ent = types.ROOT_NODE.child(ns.bsfs.Entity) + l_string = types.ROOT_LITERAL.child(ns.xsd.string) + p_annotation = types.ROOT_PREDICATE.child(ns.bsfs.Annotation, domain=n_ent, range=l_string) + p_comment = p_annotation.child(ns.bse.comment, unique=True) + self.assertEqual(Schema({p_comment}), from_string(''' + prefix rdfs: <http://www.w3.org/2000/01/rdf-schema#> + prefix xsd: <http://www.w3.org/2001/XMLSchema#> + prefix bsfs: <http://bsfs.ai/schema/> + prefix bse: <http://bsfs.ai/schema/Entity#> + + bsfs:Entity rdfs:subClassOf bsfs:Node . + xsd:string rdfs:subClassOf bsfs:Literal . + + bsfs:Annotation rdfs:subClassOf bsfs:Predicate ; + rdfs:domain bsfs:Entity ; + rdfs:range xsd:string . + + bse:comment rdfs:subClassOf bsfs:Annotation ; # inherits domain/range from bsfs:Annotation + bsfs:unique "true"^^xsd:boolean . + ''')) + + # we can define partial predicates (w/o specifying a usable range) + n_ent = types.ROOT_NODE.child(ns.bsfs.Entity) + l_string = types.ROOT_LITERAL.child(ns.xsd.string) + p_annotation = types.ROOT_PREDICATE.child(ns.bsfs.Annotation, domain=n_ent) + p_comment = p_annotation.child(ns.bse.comment, range=l_string, unique=False) + self.assertEqual(Schema({p_comment}), from_string(''' + prefix rdfs: <http://www.w3.org/2000/01/rdf-schema#> + prefix xsd: <http://www.w3.org/2001/XMLSchema#> + prefix bsfs: <http://bsfs.ai/schema/> + prefix bse: <http://bsfs.ai/schema/Entity#> + + bsfs:Entity rdfs:subClassOf bsfs:Node . + xsd:string rdfs:subClassOf bsfs:Literal . + + bsfs:Annotation rdfs:subClassOf bsfs:Predicate ; # derive predicate w/o setting range + rdfs:domain bsfs:Entity . + + bse:comment rdfs:subClassOf bsfs:Annotation ; # derived predicate w/ setting range + rdfs:range xsd:string ; + bsfs:unique "false"^^xsd:boolean . + ''')) + + # predicate definition can be split across multiple statements. + # statements can be repeated + n_ent = types.ROOT_NODE.child(ns.bsfs.Entity) + p_foo = types.ROOT_PREDICATE.child(ns.bse.foo, domain=n_ent, range=types.ROOT_NODE, unique=True) + self.assertEqual(Schema({p_foo}), from_string(''' + prefix rdfs: <http://www.w3.org/2000/01/rdf-schema#> + prefix xsd: <http://www.w3.org/2001/XMLSchema#> + prefix bsfs: <http://bsfs.ai/schema/> + prefix bse: <http://bsfs.ai/schema/Entity#> + + bsfs:Entity rdfs:subClassOf bsfs:Node . + + bse:foo rdfs:subClassOf bsfs:Predicate ; + rdfs:range bsfs:Node ; + bsfs:unique "true"^^xsd:boolean . + + bse:foo rdfs:subClassOf bsfs:Predicate ; + rdfs:domain bsfs:Entity. + ''')) + + # domain must be a subtype of parent's domain + n_ent = types.ROOT_NODE.child(ns.bsfs.Entity) + n_image = n_ent.child(ns.bsfs.Image) + p_foo = types.ROOT_PREDICATE.child(ns.bse.foo, domain=types.ROOT_NODE) + p_bar = p_foo.child(ns.bse.bar, domain=n_ent) + p_foobar = p_bar.child(ns.bse.foobar, domain=n_image) + self.assertEqual(Schema({p_foobar}), from_string(''' + prefix rdfs: <http://www.w3.org/2000/01/rdf-schema#> + prefix xsd: <http://www.w3.org/2001/XMLSchema#> + prefix bsfs: <http://bsfs.ai/schema/> + prefix bse: <http://bsfs.ai/schema/Entity#> + + bsfs:Entity rdfs:subClassOf bsfs:Node . + bsfs:Image rdfs:subClassOf bsfs:Entity . + + bse:foo rdfs:subClassOf bsfs:Predicate ; + rdfs:domain bsfs:Node . + bse:bar rdfs:subClassOf bse:foo ; + rdfs:domain bsfs:Entity . + bse:foobar rdfs:subClassOf bse:bar ; + rdfs:domain bsfs:Image . + ''')) + self.assertRaises(errors.ConsistencyError, from_string, ''' + prefix rdfs: <http://www.w3.org/2000/01/rdf-schema#> + prefix xsd: <http://www.w3.org/2001/XMLSchema#> + prefix bsfs: <http://bsfs.ai/schema/> + prefix bse: <http://bsfs.ai/schema/Entity#> + + bsfs:Entity rdfs:subClassOf bsfs:Node . + bsfs:Image rdfs:subClassOf bsfs:Entity . + + bse:foo rdfs:subClassOf bsfs:Predicate ; + rdfs:domain bsfs:Image . + bse:bar rdfs:subClassOf bse:foo ; + rdfs:domain bsfs:Entity . + bse:foobar rdfs:subClassOf bse:bar ; + rdfs:domain bsfs:Node . + ''') + + # range must be a subtype of parent's range + n_ent = types.ROOT_NODE.child(ns.bsfs.Entity) + n_image = n_ent.child(ns.bsfs.Image) + p_foo = types.ROOT_PREDICATE.child(ns.bse.foo, range=types.ROOT_NODE) + p_bar = p_foo.child(ns.bse.bar, range=n_ent) + p_foobar = p_bar.child(ns.bse.foobar, range=n_image) + self.assertEqual(Schema({p_foobar}), from_string(''' + prefix rdfs: <http://www.w3.org/2000/01/rdf-schema#> + prefix xsd: <http://www.w3.org/2001/XMLSchema#> + prefix bsfs: <http://bsfs.ai/schema/> + prefix bse: <http://bsfs.ai/schema/Entity#> + + bsfs:Entity rdfs:subClassOf bsfs:Node . + bsfs:Image rdfs:subClassOf bsfs:Entity . + + bse:foo rdfs:subClassOf bsfs:Predicate ; + rdfs:range bsfs:Node . + bse:bar rdfs:subClassOf bse:foo ; + rdfs:range bsfs:Entity . + bse:foobar rdfs:subClassOf bse:bar ; + rdfs:range bsfs:Image . + ''')) + self.assertRaises(errors.ConsistencyError, from_string, ''' + prefix rdfs: <http://www.w3.org/2000/01/rdf-schema#> + prefix xsd: <http://www.w3.org/2001/XMLSchema#> + prefix bsfs: <http://bsfs.ai/schema/> + prefix bse: <http://bsfs.ai/schema/Entity#> + + bsfs:Entity rdfs:subClassOf bsfs:Node . + bsfs:Image rdfs:subClassOf bsfs:Entity . + + bse:foo rdfs:subClassOf bsfs:Predicate ; + rdfs:range bsfs:Image . + bse:bar rdfs:subClassOf bse:foo ; + rdfs:range bsfs:Entity . + bse:foobar rdfs:subClassOf bse:bar ; + rdfs:range bsfs:Node . + ''') + + # cannot define the same predicate from multiple parents + self.assertRaises(errors.ConsistencyError, from_string, ''' + prefix rdfs: <http://www.w3.org/2000/01/rdf-schema#> + prefix xsd: <http://www.w3.org/2001/XMLSchema#> + prefix bsfs: <http://bsfs.ai/schema/> + prefix bse: <http://bsfs.ai/schema/Entity#> + + bsfs:Entity rdfs:subClassOf bsfs:Node . + bsfs:Annotation rdfs:subClassOf bsfs:Predicate . + + bse:foo rdfs:subClassOf bsfs:Predicate ; + rdfs:domain bsfs:Node ; + rdfs:range bsfs:Node ; + bsfs:unique "false"^^xsd:boolean . + + bse:foo rdfs:subClassOf bsfs:Annotation ; + rdfs:domain bsfs:Node ; + rdfs:range bsfs:Node ; + bsfs:unique "false"^^xsd:boolean . + + ''') + # cannot assign multiple conflicting domains to the same predicate + self.assertRaises(errors.ConsistencyError, from_string, ''' + prefix rdfs: <http://www.w3.org/2000/01/rdf-schema#> + prefix xsd: <http://www.w3.org/2001/XMLSchema#> + prefix bsfs: <http://bsfs.ai/schema/> + prefix bse: <http://bsfs.ai/schema/Entity#> + + bsfs:Entity rdfs:subClassOf bsfs:Node . + + bse:foo rdfs:subClassOf bsfs:Predicate ; + rdfs:domain bsfs:Node ; + rdfs:range bsfs:Node ; + bsfs:unique "false"^^xsd:boolean . + + bse:foo rdfs:subClassOf bsfs:Predicate ; + rdfs:domain bsfs:Entity . # conflicting domain + ''') + # cannot assign multiple conflicting ranges to the same predicate + self.assertRaises(errors.ConsistencyError, from_string, ''' + prefix rdfs: <http://www.w3.org/2000/01/rdf-schema#> + prefix xsd: <http://www.w3.org/2001/XMLSchema#> + prefix bsfs: <http://bsfs.ai/schema/> + prefix bse: <http://bsfs.ai/schema/Entity#> + + bsfs:Entity rdfs:subClassOf bsfs:Node . + + bse:foo rdfs:subClassOf bsfs:Predicate ; + rdfs:domain bsfs:Node ; + rdfs:range bsfs:Node ; + bsfs:unique "false"^^xsd:boolean . + + bse:foo rdfs:subClassOf bsfs:Predicate ; + rdfs:range bsfs:Entity . # conflicting range + ''') + # cannot assign multiple conflicting uniques to the same predicate + self.assertRaises(errors.ConsistencyError, from_string, ''' + prefix rdfs: <http://www.w3.org/2000/01/rdf-schema#> + prefix xsd: <http://www.w3.org/2001/XMLSchema#> + prefix bsfs: <http://bsfs.ai/schema/> + prefix bse: <http://bsfs.ai/schema/Entity#> + + bsfs:Entity rdfs:subClassOf bsfs:Node . + + bse:foo rdfs:subClassOf bsfs:Predicate ; + rdfs:domain bsfs:Node ; + rdfs:range bsfs:Node ; + bsfs:unique "false"^^xsd:boolean . + + bse:foo rdfs:subClassOf bsfs:Predicate ; + bsfs:unique "true"^^xsd:boolean . # conflicting unique + ''') + + # predicates can have annotations + self.assertDictEqual(from_string(''' + prefix rdfs: <http://www.w3.org/2000/01/rdf-schema#> + prefix xsd: <http://www.w3.org/2001/XMLSchema#> + prefix bsfs: <http://bsfs.ai/schema/> + prefix bse: <http://bsfs.ai/schema/Entity#> + + bse:comment rdfs:subClassOf bsfs:Predicate ; + rdfs:range bsfs:Node . + + ''').predicate(ns.bse.comment).annotations, {}) + self.assertDictEqual(from_string(''' + prefix rdfs: <http://www.w3.org/2000/01/rdf-schema#> + prefix xsd: <http://www.w3.org/2001/XMLSchema#> + prefix bsfs: <http://bsfs.ai/schema/> + prefix bse: <http://bsfs.ai/schema/Entity#> + + bse:comment rdfs:subClassOf bsfs:Predicate ; + rdfs:range bsfs:Node ; + rdfs:label "hello world"^^xsd:string ; + bsfs:foo "1234"^^xsd:integer . + + ''').predicate(ns.bse.comment).annotations, { + ns.rdfs.label: 'hello world', + ns.bsfs.foo: 1234, + }) + + + def test_feature(self): + # additional features can be defined + f_colors = types.ROOT_FEATURE.child(ns.bsfs.Colors) + self.assertEqual(Schema(literals={f_colors}), from_string(''' + prefix rdfs: <http://www.w3.org/2000/01/rdf-schema#> + prefix xsd: <http://www.w3.org/2001/XMLSchema#> + prefix bsfs: <http://bsfs.ai/schema/> + prefix bse: <http://bsfs.ai/schema/Entity#> + + bsfs:Array rdfs:subClassOf bsfs:Literal . + bsfs:Feature rdfs:subClassOf bsfs:Array. + + bsfs:Colors rdfs:subClassOf bsfs:Feature . + + ''')) + + # features inherit properties from parents + f_colors = types.ROOT_FEATURE.child(ns.bsfs.Colors, dimension=1234, dtype=ns.bsfs.i32) + f_main_colors = f_colors.child(ns.bsfs.MainColor, distance=ns.bsfs.cosine, dtype=ns.bsfs.f16) + self.assertEqual(Schema(literals={f_colors, f_main_colors}), from_string(''' + prefix rdfs: <http://www.w3.org/2000/01/rdf-schema#> + prefix xsd: <http://www.w3.org/2001/XMLSchema#> + prefix bsfs: <http://bsfs.ai/schema/> + prefix bse: <http://bsfs.ai/schema/Entity#> + + bsfs:Array rdfs:subClassOf bsfs:Literal . + bsfs:Feature rdfs:subClassOf bsfs:Array. + + bsfs:Colors rdfs:subClassOf bsfs:Feature ; # inherits distance from bsfs:Feature + bsfs:dimension "1234"^^xsd:integer ; # overwrites bsfs:Feature + bsfs:dtype bsfs:i32 . # overwrites bsfs:Feature + + bsfs:MainColor rdfs:subClassOf bsfs:Colors ; # inherits dimension from bsfs:Colors + bsfs:distance bsfs:cosine ; # overwrites bsfs:Feature + bsfs:dtype bsfs:f16 . # overwrites bsfs:Colors + + ''')) + + # feature definition can be split across multiple statements. + # statements can be repeated + f_colors = types.ROOT_FEATURE.child(ns.bsfs.Colors, dimension=1234, dtype=ns.bsfs.f32) + self.assertEqual(Schema(literals={f_colors}), from_string(''' + prefix rdfs: <http://www.w3.org/2000/01/rdf-schema#> + prefix xsd: <http://www.w3.org/2001/XMLSchema#> + prefix bsfs: <http://bsfs.ai/schema/> + prefix bse: <http://bsfs.ai/schema/Entity#> + + bsfs:Array rdfs:subClassOf bsfs:Literal . + bsfs:Feature rdfs:subClassOf bsfs:Array. + + bsfs:Colors rdfs:subClassOf bsfs:Feature ; + bsfs:dimension "1234"^^xsd:integer . + + bsfs:Colors rdfs:subClassOf bsfs:Feature ; + bsfs:dimension "1234"^^xsd:integer ; # non-conflicting repetition + bsfs:dtype bsfs:f32 . + ''')) + + # cannot define the same feature from multiple parents + self.assertRaises(errors.ConsistencyError, from_string, ''' + prefix rdfs: <http://www.w3.org/2000/01/rdf-schema#> + prefix xsd: <http://www.w3.org/2001/XMLSchema#> + prefix bsfs: <http://bsfs.ai/schema/> + prefix bse: <http://bsfs.ai/schema/Entity#> + + bsfs:Array rdfs:subClassOf bsfs:Literal . + bsfs:Feature rdfs:subClassOf bsfs:Array. + bsfs:ColorSpace rdfs:subClassOf bsfs:Feature . + + bsfs:Colors rdfs:subClassOf bsfs:Feature . + bsfs:Colors rdfs:subClassOf bsfs:ColorSpace . + + ''') + # cannot assign multiple conflicting dimensions to the same feature + self.assertRaises(errors.ConsistencyError, from_string, ''' + prefix rdfs: <http://www.w3.org/2000/01/rdf-schema#> + prefix xsd: <http://www.w3.org/2001/XMLSchema#> + prefix bsfs: <http://bsfs.ai/schema/> + prefix bse: <http://bsfs.ai/schema/Entity#> + + bsfs:Array rdfs:subClassOf bsfs:Literal . + bsfs:Feature rdfs:subClassOf bsfs:Array. + + bsfs:Colors rdfs:subClassOf bsfs:Feature ; + bsfs:dimension "1234"^^xsd:integer . + + bsfs:Colors rdfs:subClassOf bsfs:Feature ; + bsfs:dimension "4321"^^xsd:integer . # conflicting dimension + + ''') + # cannot assign multiple conflicting dtypes to the same feature + self.assertRaises(errors.ConsistencyError, from_string, ''' + prefix rdfs: <http://www.w3.org/2000/01/rdf-schema#> + prefix xsd: <http://www.w3.org/2001/XMLSchema#> + prefix bsfs: <http://bsfs.ai/schema/> + prefix bse: <http://bsfs.ai/schema/Entity#> + + bsfs:Array rdfs:subClassOf bsfs:Literal . + bsfs:Feature rdfs:subClassOf bsfs:Array. + + bsfs:Colors rdfs:subClassOf bsfs:Feature ; + bsfs:dtype bsfs:f32 . + + bsfs:Colors rdfs:subClassOf bsfs:Feature ; + bsfs:dtype bsfs:f16 . # conflicting dtype + ''') + # cannot assign multiple conflicting distance metrics to the same feature + self.assertRaises(errors.ConsistencyError, from_string, ''' + prefix rdfs: <http://www.w3.org/2000/01/rdf-schema#> + prefix xsd: <http://www.w3.org/2001/XMLSchema#> + prefix bsfs: <http://bsfs.ai/schema/> + prefix bse: <http://bsfs.ai/schema/Entity#> + + bsfs:Array rdfs:subClassOf bsfs:Literal . + bsfs:Feature rdfs:subClassOf bsfs:Array. + + bsfs:Colors rdfs:subClassOf bsfs:Feature ; + bsfs:distance bsfs:euclidean . + + bsfs:Colors rdfs:subClassOf bsfs:Feature ; + bsfs:distance bsfs:cosine . # conflicting distance + ''') + + # features can have annotations + self.assertDictEqual(from_string(''' + prefix rdfs: <http://www.w3.org/2000/01/rdf-schema#> + prefix xsd: <http://www.w3.org/2001/XMLSchema#> + prefix bsfs: <http://bsfs.ai/schema/> + prefix bse: <http://bsfs.ai/schema/Entity#> + + bsfs:Array rdfs:subClassOf bsfs:Literal . + bsfs:Feature rdfs:subClassOf bsfs:Array. + + bsfs:Colors rdfs:subClassOf bsfs:Feature ; + bsfs:dimension "1234"^^xsd:integer . + + ''').literal(ns.bsfs.Colors).annotations, {}) + self.assertDictEqual(from_string(''' + prefix rdfs: <http://www.w3.org/2000/01/rdf-schema#> + prefix xsd: <http://www.w3.org/2001/XMLSchema#> + prefix bsfs: <http://bsfs.ai/schema/> + prefix bse: <http://bsfs.ai/schema/Entity#> + + bsfs:Array rdfs:subClassOf bsfs:Literal . + bsfs:Feature rdfs:subClassOf bsfs:Array. + + bsfs:Colors rdfs:subClassOf bsfs:Feature ; + bsfs:dimension "1234"^^xsd:integer ; + rdfs:label "hello world"^^xsd:string ; + bsfs:foo "1234"^^xsd:integer . + + ''').literal(ns.bsfs.Colors).annotations, { + ns.rdfs.label: 'hello world', + ns.bsfs.foo: 1234, + }) + + + def test_integration(self): + # nodes + n_ent = types.ROOT_NODE.child(ns.bsfs.Entity) + n_tag = types.ROOT_NODE.child(ns.bsfs.Tag) + n_image = n_ent.child(ns.bsfs.Image) + # literals + l_string = types.ROOT_LITERAL.child(ns.xsd.string) + l_array = types.ROOT_LITERAL.child(ns.bsfs.array) + l_integer = types.ROOT_NUMBER.child(ns.xsd.integer) + l_boolean = types.ROOT_LITERAL.child(ns.xsd.boolean) + # predicates + p_annotation = types.ROOT_PREDICATE.child(ns.bsfs.Annotation) + p_tag = types.ROOT_PREDICATE.child(ns.bse.tag, domain=n_ent, range=n_tag) + p_group = p_tag.child(ns.bse.group, domain=n_image, unique=True) + p_comment = p_annotation.child(ns.bse.comment, range=l_string) + # features + f_colors = types.ROOT_FEATURE.child(URI('http://bsfs.ai/schema/Feature/colors_spatial'), + dtype=ns.bsfs.f16, distance=ns.bsfs.euclidean) + f_colors1234 = f_colors.child(URI('http://bsfs.ai/schema/Feature/colors_spatial#1234'), dimension=1024) + f_colors4321 = f_colors.child(URI('http://bsfs.ai/schema/Feature/colors_spatial#4321'), dimension=2048) + # schema + ref = Schema( + {p_annotation, p_tag, p_group, p_comment}, + {n_ent, n_tag, n_image}, + {l_string, l_integer, l_boolean, f_colors, f_colors1234, f_colors4321}) + # load from string + gen = from_string(''' + # generic prefixes + prefix rdfs: <http://www.w3.org/2000/01/rdf-schema#> + prefix xsd: <http://www.w3.org/2001/XMLSchema#> + + # bsfs prefixes + prefix bsfs: <http://bsfs.ai/schema/> + prefix bse: <http://bsfs.ai/schema/Entity#> + + # nodes + bsfs:Entity rdfs:subClassOf bsfs:Node ; + rdfs:label "Principal node"^^xsd:string . + bsfs:Tag rdfs:subClassOf bsfs:Node ; + rdfs:label "Tag"^^xsd:string . + bsfs:Image rdfs:subClassOf bsfs:Entity . + + # literals + xsd:string rdfs:subClassOf bsfs:Literal ; + rdfs:label "A sequence of characters"^^xsd:string . + bsfs:Array rdfs:subClassOf bsfs:Literal . + bsfs:Feature rdfs:subClassOf bsfs:Array. + bsfs:Number rdfs:subClassOf bsfs:Literal . + xsd:integer rdfs:subClassOf bsfs:Number . + xsd:boolean rdfs:subClassOf bsfs:Literal . + + + # abstract predicates + bsfs:Annotation rdfs:subClassOf bsfs:Predicate ; + rdfs:label "node annotation"^^xsd:string . + + # feature instances + <http://bsfs.ai/schema/Feature/colors_spatial> rdfs:subClassOf bsfs:Feature ; + bsfs:dtype bsfs:f16 ; + bsfs:distance bsfs:euclidean ; + # annotations + rdfs:label "ColorsSpatial instances. Dimension depends on instance."^^xsd:string ; + bsfs:first_arg "1234"^^xsd:integer ; + bsfs:second_arg "hello world"^^xsd:string . + + <http://bsfs.ai/schema/Feature/colors_spatial#1234> rdfs:subClassOf <http://bsfs.ai/schema/Feature/colors_spatial> ; + bsfs:dimension "1024"^^xsd:integer ; + rdfs:label "Main colors spatial instance"^^xsd:string . + + <http://bsfs.ai/schema/Feature/colors_spatial#4321> rdfs:subClassOf <http://bsfs.ai/schema/Feature/colors_spatial> ; + bsfs:dimension "2048"^^xsd:integer . + + # predicate instances + bse:tag rdfs:subClassOf bsfs:Predicate ; + rdfs:domain bsfs:Entity ; + rdfs:range bsfs:Tag ; + bsfs:unique "false"^^xsd:boolean ; + # annotations + rdfs:label "connect entity to a tag"^^xsd:string . + + bse:group rdfs:subClassOf bse:tag ; # subtype of another predicate + rdfs:domain bsfs:Image ; + bsfs:unique "true"^^xsd:boolean . + + bse:comment rdfs:subClassOf bsfs:Annotation ; # subtype of abstract predicate + rdfs:domain bsfs:Node ; + rdfs:range xsd:string ; + bsfs:unique "false"^^xsd:boolean . + + ''') + # schemas are equal + self.assertEqual(ref, gen) + # check annotations + self.assertDictEqual(gen.node(ns.bsfs.Entity).annotations, {ns.rdfs.label: 'Principal node'}) + self.assertDictEqual(gen.node(ns.bsfs.Tag).annotations, {ns.rdfs.label: 'Tag'}) + self.assertDictEqual(gen.literal(ns.xsd.string).annotations, {ns.rdfs.label: 'A sequence of characters'}) + self.assertDictEqual(gen.predicate(ns.bsfs.Annotation).annotations, {ns.rdfs.label: 'node annotation'}) + self.assertDictEqual(gen.literal(URI('http://bsfs.ai/schema/Feature/colors_spatial')).annotations, { + ns.rdfs.label: 'ColorsSpatial instances. Dimension depends on instance.', + ns.bsfs.first_arg: 1234, + ns.bsfs.second_arg: 'hello world', + }) + self.assertDictEqual(gen.literal(URI('http://bsfs.ai/schema/Feature/colors_spatial#1234')).annotations, { + ns.rdfs.label: 'Main colors spatial instance'}) + self.assertDictEqual(gen.predicate(ns.bse.tag).annotations, {ns.rdfs.label: 'connect entity to a tag'}) + + # blank nodes result in an error + self.assertRaises(errors.BackendError, from_string, ''' + prefix rdfs: <http://www.w3.org/2000/01/rdf-schema#> + prefix bsfs: <http://bsfs.ai/schema/> + bsfs:Entity rdfs:subClassOf bsfs:Node ; + bsfs:foo _:bar . + ''') + + +class TestToString(unittest.TestCase): + + def test_empty(self): + self.assertEqual(Schema(), from_string(to_string(Schema()))) + + def test_parse(self): + schema = Schema() + schema._nodes[ns.bsfs.Invalid] = 123 # NOTE: Access protected to force an invalid schema + self.assertRaises(TypeError, to_string, schema) + + def test_literal(self): + # root literals + l_str = types.ROOT_LITERAL.child(ns.xsd.string) + # derived literals + l_int = types.ROOT_NUMBER.child(ns.xsd.integer) + l_unsigned = l_int.child(ns.xsd.unsigned) + # create schema + schema = Schema(literals={l_int, l_str, l_unsigned}) + + schema_str = to_string(schema) + # all symbols are serialized + self.assertIn('xsd:string', schema_str) + self.assertIn('xsd:integer', schema_str) + self.assertIn('xsd:unsigned', schema_str) + # unserialize yields the original schema + self.assertEqual(schema, from_string(schema_str)) + + # literals that have no parent are ignored + schema = Schema(literals={types.Literal(ns.bsfs.Invalid, None)}) + self.assertEqual(Schema(), from_string(to_string(schema))) + self.assertNotIn('Invalid', to_string(schema)) + + # literal annotations are serialized + annotations = { + ns.rdfs.label: 'hello world', + ns.schema.description: 'some text', + ns.bsfs.foo: 1234, + ns.bsfs.bar: True, + } + l_str = types.ROOT_LITERAL.child(ns.xsd.string, **annotations) + self.assertDictEqual( + annotations, + from_string(to_string(Schema(literals={l_str}))).literal(ns.xsd.string).annotations) + + + def test_node(self): + # root nodes + n_ent = types.ROOT_NODE.child(ns.bsfs.Entity) + n_tag = types.ROOT_NODE.child(ns.bsfs.Tag) + # derived nodes + n_img = n_ent.child(ns.bsfs.Image) + n_doc = n_ent.child(ns.bsfs.Document) + n_grp = n_tag.child(ns.bsfs.Group) + # create schema + schema = Schema(nodes={n_ent, n_img, n_doc, n_tag, n_grp}) + + schema_str = to_string(schema) + # all symbols are serialized + self.assertIn('bsfs:Entity', schema_str) + self.assertIn('bsfs:Tag', schema_str) + self.assertIn('bsfs:Image', schema_str) + self.assertIn('bsfs:Document', schema_str) + self.assertIn('bsfs:Group', schema_str) + # unserialize yields the original schema + self.assertEqual(schema, from_string(schema_str)) + + # nodes that have no parent are ignored + schema = Schema(nodes={types.Node(ns.bsfs.Invalid, None)}) + self.assertEqual(Schema(), from_string(to_string(schema))) + self.assertNotIn('Invalid', to_string(schema)) + + # node annotations are serialized + annotations = { + ns.rdfs.label: 'hello world', + ns.schema.description: 'some text', + ns.bsfs.foo: 1234, + ns.bsfs.bar: True, + } + n_ent = types.ROOT_NODE.child(ns.bsfs.Entity, **annotations) + self.assertDictEqual( + annotations, + from_string(to_string(Schema(nodes={n_ent}))).node(ns.bsfs.Entity).annotations) + + + def test_predicate(self): + # auxiliary types + n_ent = types.ROOT_NODE.child(ns.bsfs.Entity) + l_str = types.ROOT_LITERAL.child(ns.xsd.string) + # root predicates + p_annotation = types.ROOT_PREDICATE.child(ns.bsfs.Annotation, domain=n_ent) + p_owner = types.ROOT_PREDICATE.child(ns.bse.owner, range=l_str, unique=True) + # derived predicates + p_comment = p_annotation.child(ns.bse.comment, range=l_str) # inherits domain + p_note = p_comment.child(ns.bse.note, unique=True) # inherits domain/range + # create schema + schema = Schema({p_owner, p_comment, p_note}) + + schema_str = to_string(schema) + # all symbols are serialized + self.assertIn('bsfs:Entity', schema_str) + self.assertIn('xsd:string', schema_str) + self.assertIn('bsfs:Annotation', schema_str) + self.assertIn('bse:comment', schema_str) + self.assertIn('bse:owner', schema_str) + self.assertIn('bse:note', schema_str) + # inherited properties are not serialized + self.assertIsNotNone(re.search(r'bse:comment[^\.]*rdfs:range[^\.]', schema_str)) + self.assertIsNone(re.search(r'bse:comment[^\.]*rdfs:domain[^\.]', schema_str)) + #p_note has no domain/range + self.assertIsNone(re.search(r'bse:note[^\.]*rdfs:domain[^\.]', schema_str)) + self.assertIsNone(re.search(r'bse:note[^\.]*rdfs:range[^\.]', schema_str)) + # unserialize yields the original schema + self.assertEqual(schema, from_string(schema_str)) + + # predicate annotations are serialized + annotations = { + ns.rdfs.label: 'hello world', + ns.schema.description: 'some text', + ns.bsfs.foo: 1234, + ns.bsfs.bar: False, + } + p_annotation = types.ROOT_PREDICATE.child(ns.bsfs.Annotation, **annotations) + self.assertDictEqual( + annotations, + from_string(to_string(Schema({p_annotation}))).predicate(ns.bsfs.Annotation).annotations) + + + def test_feature(self): + # root features + f_colors = types.ROOT_FEATURE.child(URI('http://bsfs.ai/schema/Feature/colors'), + distance=ns.bsfs.cosine) + # derived features + f_colors1234 = f_colors.child(URI('http://bsfs.ai/schema/Feature/colors#1234'), + dimension=1024) # inherits dtype, distance + f_colors4321 = f_colors.child(URI('http://bsfs.ai/schema/Feature/colors#4321'), + dimension=2048, distance=ns.bsfs.euclidean) # inherits dtype + # create schema + schema = Schema(literals={f_colors, f_colors1234, f_colors4321}) + + schema_str = to_string(schema) + # all symbols are serialized + self.assertIn('bsfs:Array', schema_str) + self.assertIn('<http://bsfs.ai/schema/Feature/colors', schema_str) + self.assertIn('<http://bsfs.ai/schema/Feature/colors#1234', schema_str) + self.assertIn('<http://bsfs.ai/schema/Feature/colors#4321', schema_str) + # inherited properties are not serialized + self.assertIsNotNone(re.search(r'<http://bsfs\.ai/schema/Feature/colors#1234>[^\.]*bsfs:dimension[^\.]', schema_str)) + self.assertIsNone(re.search(r'<http://bsfs\.ai/schema/Feature/colors#1234>[^\.]*bsfs:dtype[^\.]', schema_str)) + self.assertIsNone(re.search(r'<http://bsfs\.ai/schema/Feature/colors#1234>[^\.]*bsfs:distance[^\.]', schema_str)) + self.assertIsNotNone(re.search(r'<http://bsfs\.ai/schema/Feature/colors#4321>[^\.]*bsfs:dimension[^\.]', schema_str)) + self.assertIsNotNone(re.search(r'<http://bsfs\.ai/schema/Feature/colors#4321>[^\.]*bsfs:distance[^\.]', schema_str)) + self.assertIsNone(re.search(r'<http://bsfs\.ai/schema/Feature/colors#4321>[^\.]*bsfs:dtype[^\.]', schema_str)) + # unserialize yields the original schema + self.assertEqual(schema, from_string(schema_str)) + + # predicate annotations are serialized + annotations = { + ns.rdfs.label: 'hello world', + ns.schema.description: 'some text', + ns.bsfs.foo: 1234, + ns.bsfs.bar: False, + } + f_colors = types.ROOT_FEATURE.child(URI('http://bsfs.ai/schema/Feature/colors'), + dtype=ns.bsfs.f16, distance=ns.bsfs.euclidean, + **annotations) + self.assertDictEqual( + annotations, + from_string(to_string(Schema(literals={f_colors}))).literal(URI('http://bsfs.ai/schema/Feature/colors')).annotations) + + +## main ## + +if __name__ == '__main__': + unittest.main() + +## EOF ## |