aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
-rw-r--r--bsfs/graph/ac/base.py4
-rw-r--r--bsfs/graph/ac/null.py5
-rw-r--r--bsfs/graph/graph.py28
-rw-r--r--bsfs/graph/resolve.py161
-rw-r--r--test/graph/ac/test_null.py10
-rw-r--r--test/graph/test_graph.py55
-rw-r--r--test/graph/test_nodes.py30
-rw-r--r--test/graph/test_resolve.py181
8 files changed, 459 insertions, 15 deletions
diff --git a/bsfs/graph/ac/base.py b/bsfs/graph/ac/base.py
index bc9aeb3..0703e2e 100644
--- a/bsfs/graph/ac/base.py
+++ b/bsfs/graph/ac/base.py
@@ -10,6 +10,7 @@ import typing
# bsfs imports
from bsfs import schema
+from bsfs.query import ast
from bsfs.triple_store import TripleStoreBase
from bsfs.utils import URI
@@ -67,5 +68,8 @@ class AccessControlBase(abc.ABC):
def createable(self, node_type: schema.Node, guids: typing.Iterable[URI]) -> typing.Iterable[URI]:
"""Return nodes that are allowed to be created."""
+ @abc.abstractmethod
+ def filter_read(self, node_type: schema.Node, query: ast.filter.FilterExpression) -> ast.filter.FilterExpression:
+ """Re-write a filter *query* to get (i.e., read) *node_type* nodes."""
## EOF ##
diff --git a/bsfs/graph/ac/null.py b/bsfs/graph/ac/null.py
index 36838bd..12b4e87 100644
--- a/bsfs/graph/ac/null.py
+++ b/bsfs/graph/ac/null.py
@@ -10,6 +10,7 @@ import typing
# bsfs imports
from bsfs import schema
from bsfs.namespace import ns
+from bsfs.query import ast
from bsfs.utils import URI
# inner-module imports
@@ -49,4 +50,8 @@ class NullAC(base.AccessControlBase):
"""Return nodes that are allowed to be created."""
return guids
+ def filter_read(self, node_type: schema.Node, query: ast.filter.FilterExpression) -> ast.filter.FilterExpression:
+ """Re-write a filter *query* to get (i.e., read) *node_type* nodes."""
+ return query
+
## EOF ##
diff --git a/bsfs/graph/graph.py b/bsfs/graph/graph.py
index 51fe75d..f030fed 100644
--- a/bsfs/graph/graph.py
+++ b/bsfs/graph/graph.py
@@ -9,13 +9,15 @@ import os
import typing
# bsfs imports
-from bsfs.query import ast
+from bsfs.query import ast, validate
from bsfs.schema import Schema
from bsfs.triple_store import TripleStoreBase
from bsfs.utils import URI, typename
# inner-module imports
+from . import ac
from . import nodes as _nodes
+from . import resolve
# exports
__all__: typing.Sequence[str] = (
@@ -44,6 +46,9 @@ class Graph():
def __init__(self, backend: TripleStoreBase, user: URI):
self._backend = backend
self._user = user
+ self._resolver = resolve.Filter(self._backend.schema)
+ self._validate = validate.Filter(self._backend.schema)
+ self._ac = ac.NullAC(self._backend, self._user)
# ensure Graph schema requirements
self.migrate(self._backend.schema)
@@ -85,6 +90,9 @@ class Graph():
# migrate schema in backend
# FIXME: consult access controls!
self._backend.schema = schema
+ # re-initialize members
+ self._resolver.schema = self.schema
+ self._validate.schema = self.schema
# return self
return self
@@ -108,11 +116,21 @@ class Graph():
*node_type*) once some data is assigned to them.
"""
- type_ = self.schema.node(node_type)
- return _nodes.Nodes(self._backend, self._user, type_, {guid})
+ return self.nodes(node_type, {guid})
- def get(self, node_type: URI, subject: ast.filter.FilterExpression) -> _nodes.Nodes:
+ def get(self, node_type: URI, query: ast.filter.FilterExpression) -> _nodes.Nodes: # FIXME: How about empty query?
"""Return a `Nodes` instance over all nodes of type *node_type* that match the *subject* query."""
- raise NotImplementedError()
+ # get node type
+ type_ = self.schema.node(node_type)
+ # resolve Nodes instances
+ query = self._resolver(type_, query)
+ # add access controls to query
+ query = self._ac.filter_read(type_, query)
+ # validate query
+ self._validate(type_, query)
+ # query the backend
+ guids = self._backend.get(type_, query) # no need to materialize
+ # return Nodes instance
+ return _nodes.Nodes(self._backend, self._user, type_, guids)
## EOF ##
diff --git a/bsfs/graph/resolve.py b/bsfs/graph/resolve.py
new file mode 100644
index 0000000..feb0855
--- /dev/null
+++ b/bsfs/graph/resolve.py
@@ -0,0 +1,161 @@
+"""
+
+Part of the BlackStar filesystem (bsfs) module.
+A copy of the license is provided with the project.
+Author: Matthias Baumgartner, 2022
+"""
+# imports
+import typing
+
+# bsfs imports
+from bsfs import schema as bsc
+from bsfs.query import ast
+from bsfs.utils import errors
+
+# inner-module imports
+from . import nodes
+
+# exports
+__all__: typing.Sequence[str] = (
+ 'Filter',
+ )
+
+
+## code ##
+
+class Filter():
+ """Rewrites the query to replace `bsfs.graph.nodes.Nodes` instances with the respective URI.
+ Does only limited type checking and schema validation.
+ Use `bsfs.schema.validate.Filter` to do so.
+
+ Example:
+ input: Any(ns.bse.tag, Is(Nodes(...)))
+ output: Any(ns.bse.tag, Or(Is(...), Is(...), ...)))
+
+ >>> tags = graph.node(ns.bsfs.Tag, 'http://example.com/me/tag#1234')
+ >>> graph.get(ns.bsfs.Entity, ast.filter.Any(ns.bse.tag, ast.filter.Is(tags)))
+
+ """
+
+ T_VERTEX = typing.Union[bsc.Node, bsc.Literal]
+
+ def __init__(self, schema):
+ self.schema = schema
+
+ def __call__(self, root_type: bsc.Node, node: ast.filter.FilterExpression):
+ return self._parse_filter_expression(root_type, node)
+
+ def _parse_filter_expression(
+ self,
+ type_: T_VERTEX,
+ node: ast.filter.FilterExpression,
+ ) -> ast.filter.FilterExpression:
+ """Route *node* to the handler of the respective FilterExpression subclass."""
+ if isinstance(node, ast.filter.Is):
+ return self._is(type_, node)
+ if isinstance(node, ast.filter.Not):
+ return self._not(type_, node)
+ if isinstance(node, ast.filter.Has):
+ return self._has(type_, node)
+ if isinstance(node, ast.filter.Any):
+ return self._any(type_, node)
+ if isinstance(node, ast.filter.All):
+ return self._all(type_, node)
+ if isinstance(node, ast.filter.And):
+ return self._and(type_, node)
+ if isinstance(node, ast.filter.Or):
+ return self._or(type_, node)
+ if isinstance(node, (ast.filter.Equals, ast.filter.Substring, \
+ ast.filter.StartsWith, ast.filter.EndsWith)):
+ return self._value(type_, node)
+ if isinstance(node, (ast.filter.LessThan, ast.filter.GreaterThan)):
+ return self._bounded(type_, node)
+ # invalid node
+ raise errors.BackendError(f'expected filter expression, found {node}')
+
+ def _parse_predicate_expression(self, node: ast.filter.PredicateExpression) -> T_VERTEX:
+ """Route *node* to the handler of the respective PredicateExpression subclass."""
+ if isinstance(node, ast.filter.Predicate):
+ return self._predicate(node)
+ if isinstance(node, ast.filter.OneOf):
+ return self._one_of(node)
+ # invalid node
+ raise errors.BackendError(f'expected predicate expression, found {node}')
+
+ def _predicate(self, node: ast.filter.Predicate) -> T_VERTEX:
+ if not self.schema.has_predicate(node.predicate):
+ raise errors.ConsistencyError(f'predicate {node.predicate} is not in the schema')
+ pred = self.schema.predicate(node.predicate)
+ dom, rng = pred.domain, pred.range
+ if node.reverse:
+ dom, rng = rng, dom
+ return rng
+
+ def _one_of(self, node: ast.filter.OneOf) -> T_VERTEX:
+ # determine domain and range types
+ rng = None
+ for pred in node:
+ # parse child expression
+ subrng = self._parse_predicate_expression(pred)
+ # determine the next type
+ try:
+ if rng is None or subrng > rng: # pick most generic range
+ rng = subrng
+ except TypeError as err:
+ raise errors.ConsistencyError(f'ranges {subrng} and {rng} are not related') from err
+ if rng is None:
+ raise errors.UnreachableError()
+ return rng
+
+ def _any(self, type_: T_VERTEX, node: ast.filter.Any) -> ast.filter.Any: # pylint: disable=unused-argument
+ next_type = self._parse_predicate_expression(node.predicate)
+ return ast.filter.Any(node.predicate, self._parse_filter_expression(next_type, node.expr))
+
+ def _all(self, type_: T_VERTEX, node: ast.filter.All) -> ast.filter.All: # pylint: disable=unused-argument
+ next_type = self._parse_predicate_expression(node.predicate)
+ return ast.filter.All(node.predicate, self._parse_filter_expression(next_type, node.expr))
+
+ def _and(self, type_: T_VERTEX, node: ast.filter.And) -> ast.filter.And:
+ return ast.filter.And({self._parse_filter_expression(type_, expr) for expr in node})
+
+ def _or(self, type_: T_VERTEX, node: ast.filter.Or) -> ast.filter.Or:
+ return ast.filter.Or({self._parse_filter_expression(type_, expr) for expr in node})
+
+ def _not(self, type_: T_VERTEX, node: ast.filter.Not) -> ast.filter.Not:
+ return ast.filter.Not(self._parse_filter_expression(type_, node.expr))
+
+ def _has(self, type_: T_VERTEX, node: ast.filter.Has) -> ast.filter.Has: # pylint: disable=unused-argument
+ return node
+
+ def _value(self, type_: T_VERTEX, node: ast.filter._Value) -> ast.filter._Value: # pylint: disable=unused-argument
+ return node
+
+ def _bounded(self, type_: T_VERTEX, node: ast.filter._Bounded) -> ast.filter._Bounded: # pylint: disable=unused-argument
+ return node
+
+ def _is(self, type_: T_VERTEX, node: ast.filter.Is) -> typing.Union[ast.filter.Or, ast.filter.Is]:
+ # check if action is needed
+ if not isinstance(node.value, nodes.Nodes):
+ return node
+ # check schema consistency
+ if node.value.node_type not in self.schema.nodes():
+ raise errors.ConsistencyError(f'node {node.value.node_type} is not in the schema')
+ # check type compatibility
+ if not isinstance(type_, bsc.Node):
+ raise errors.ConsistencyError(f'expected a node, found {type_}')
+ if not node.value.node_type <= type_:
+ raise errors.ConsistencyError(f'expected type {type_} or subtype thereof, found {node.value.node_type}')
+ # NOTE: We assume that the node type is checked when writing to the backend.
+ # Links to any of the guids can therefore only exist if the type matches.
+ # Hence, we don't add a type check/constrain here.
+ return ast.filter.Or(ast.filter.Is(guid) for guid in node.value.guids)
+ # optimized code, removing unnecessary ast.filter.Or
+ #guids = set(node.value.guids)
+ #if len(guids) == 0:
+ # raise errors.BackendError(f'')
+ #if len(guids) == 1:
+ # return ast.filter.Nodeid(next(iter(guids)))
+ #return ast.filter.Or(ast.filter.Is(guid) for guid in guids)
+
+
+## EOF ##
diff --git a/test/graph/ac/test_null.py b/test/graph/ac/test_null.py
index f39c9be..c863943 100644
--- a/test/graph/ac/test_null.py
+++ b/test/graph/ac/test_null.py
@@ -10,6 +10,7 @@ import unittest
# bsie imports
from bsfs import schema as _schema
from bsfs.namespace import ns
+from bsfs.query import ast
from bsfs.triple_store import SparqlStore
from bsfs.utils import URI
@@ -93,6 +94,15 @@ class TestNullAC(unittest.TestCase):
ac = NullAC(self.backend, self.user)
self.assertSetEqual(self.ent_ids, ac.createable(self.ent_type, self.ent_ids))
+ def test_filter_read(self):
+ query = ast.filter.Or(
+ ast.filter.Any(ns.bse.tag, ast.filter.Is('http://example.com/tag#1234')),
+ ast.filter.Any(ns.bse.tag, ast.filter.Is('http://example.com/tag#4321')),
+ ast.filter.Any(ns.bse.author, ast.filter.Equals('Me, Myself, and I')))
+ ac = NullAC(self.backend, self.user)
+ self.assertEqual(query, ac.filter_read(self.ent_type, query))
+ return query
+
## main ##
diff --git a/test/graph/test_graph.py b/test/graph/test_graph.py
index 0a3fd5b..8503d5b 100644
--- a/test/graph/test_graph.py
+++ b/test/graph/test_graph.py
@@ -9,10 +9,11 @@ import unittest
# bsie imports
from bsfs import schema
+from bsfs.graph.nodes import Nodes
from bsfs.namespace import ns
+from bsfs.query import ast
from bsfs.triple_store import SparqlStore
from bsfs.utils import URI, errors
-from bsfs.graph.nodes import Nodes
# objects to test
from bsfs.graph.graph import Graph
@@ -193,7 +194,57 @@ class TestGraph(unittest.TestCase):
'''))
def test_get(self):
- raise NotImplementedError()
+ # setup
+ graph = Graph(self.backend, self.user)
+ graph.migrate(schema.Schema.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:Tag rdfs:subClassOf bsfs:Node .
+ xsd:string rdfs:subClassOf bsfs:Literal .
+
+ bse:tag rdfs:subClassOf bsfs:Predicate ;
+ rdfs:domain bsfs:Entity ;
+ rdfs:range bsfs:Tag ;
+ bsfs:unique "false"^^xsd:boolean .
+
+ bse:comment rdfs:subClassOf bsfs:Predicate ;
+ rdfs:domain bsfs:Node ;
+ rdfs:range xsd:string ;
+ bsfs:unique "false"^^xsd:boolean .
+
+ '''))
+ # add some instances
+ ents = graph.nodes(ns.bsfs.Entity, {URI('http://example.com/entity#1234'), URI('http://example.com/entity#4321')})
+ tags = graph.nodes(ns.bsfs.Tag, {URI('http://example.com/tag#1234'), URI('http://example.com/tag#4321')})
+ # add some node links
+ ents.set(ns.bse.tag, tags)
+ # add some literals
+ graph.node(ns.bsfs.Entity, URI('http://example.com/entity#1234')).set(ns.bse.comment, 'hello world')
+ graph.node(ns.bsfs.Entity, URI('http://example.com/entity#1234')).set(ns.bse.comment, 'foo')
+ graph.node(ns.bsfs.Entity, URI('http://example.com/entity#1234')).set(ns.bse.comment, 'foobar')
+ graph.node(ns.bsfs.Tag, URI('http://example.com/tag#1234')).set(ns.bse.comment, 'foo')
+ graph.node(ns.bsfs.Tag, URI('http://example.com/tag#4321')).set(ns.bse.comment, 'bar')
+
+ # get exception for invalid query
+ self.assertRaises(errors.ConsistencyError, graph.get, ns.bsfs.Entity, ast.filter.Any(ns.bse.tag, ast.filter.Equals('hello world')))
+
+ # query returns nodes
+ self.assertEqual(graph.get(ns.bsfs.Entity, ast.filter.Any(ns.bse.tag, ast.filter.Is(tags))), ents)
+ self.assertEqual(graph.get(ns.bsfs.Entity, ast.filter.Any(ns.bse.comment, ast.filter.StartsWith('foo'))),
+ graph.node(ns.bsfs.Entity, URI('http://example.com/entity#1234')))
+ self.assertEqual(graph.get(ns.bsfs.Node, ast.filter.Any(ns.bse.comment, ast.filter.StartsWith('foo'))),
+ graph.nodes(ns.bsfs.Node, {URI('http://example.com/entity#1234'), URI('http://example.com/tag#1234')}))
+ self.assertEqual(graph.get(ns.bsfs.Entity, ast.filter.Or(
+ ast.filter.Any(ns.bse.comment, ast.filter.EndsWith('bar')),
+ ast.filter.Any(ns.bse.tag, ast.filter.All(ns.bse.comment, ast.filter.Equals('bar'))))),
+ ents)
+
+
+
## main ##
diff --git a/test/graph/test_nodes.py b/test/graph/test_nodes.py
index 43e7f6f..11ae46d 100644
--- a/test/graph/test_nodes.py
+++ b/test/graph/test_nodes.py
@@ -72,6 +72,20 @@ class TestNodes(unittest.TestCase):
bsfs:unique "true"^^xsd:boolean .
''')
+ self.schema_triples = {
+ # schema hierarchy
+ (rdflib.URIRef(ns.bsfs.Entity), rdflib.RDFS.subClassOf, rdflib.URIRef(ns.bsfs.Node)),
+ (rdflib.URIRef(ns.bsfs.Tag), rdflib.RDFS.subClassOf, rdflib.URIRef(ns.bsfs.Node)),
+ (rdflib.URIRef(ns.bsfs.User), rdflib.RDFS.subClassOf, rdflib.URIRef(ns.bsfs.Node)),
+ (rdflib.URIRef(ns.xsd.string), rdflib.RDFS.subClassOf, rdflib.URIRef(ns.bsfs.Literal)),
+ (rdflib.URIRef(ns.xsd.integer), rdflib.RDFS.subClassOf, rdflib.URIRef(ns.bsfs.Literal)),
+ (rdflib.URIRef(ns.bsm.t_created), rdflib.RDFS.subClassOf, rdflib.URIRef(ns.bsfs.Predicate)),
+ (rdflib.URIRef(ns.bse.comment), rdflib.RDFS.subClassOf, rdflib.URIRef(ns.bsfs.Predicate)),
+ (rdflib.URIRef(ns.bse.filesize), rdflib.RDFS.subClassOf, rdflib.URIRef(ns.bsfs.Predicate)),
+ (rdflib.URIRef(ns.bse.tag), rdflib.RDFS.subClassOf, rdflib.URIRef(ns.bsfs.Predicate)),
+ (rdflib.URIRef(ns.bse.author), rdflib.RDFS.subClassOf, rdflib.URIRef(ns.bsfs.Predicate)),
+ (rdflib.URIRef('http://bsfs.ai/schema/Tag#representative'), rdflib.RDFS.subClassOf, rdflib.URIRef(ns.bsfs.Predicate)),
+ }
# Nodes constructor args
self.user = URI('http://example.com/me')
# set args
@@ -160,7 +174,7 @@ class TestNodes(unittest.TestCase):
time_triples = list(self.backend._graph.objects(rdflib.URIRef('http://example.com/me/entity#1234'), rdflib.URIRef(self.t_created.uri)))
t_ent_created = float(time_triples[0]) if len(time_triples) > 0 else 0.0
# check triples
- self.assertSetEqual(set(self.backend._graph), {
+ self.assertSetEqual(set(self.backend._graph), self.schema_triples | {
# entity definitions
(rdflib.URIRef('http://example.com/me/entity#1234'), rdflib.RDF.type, rdflib.URIRef('http://bsfs.ai/schema/Entity')),
(rdflib.URIRef('http://example.com/me/entity#4321'), rdflib.RDF.type, rdflib.URIRef('http://bsfs.ai/schema/Entity')),
@@ -171,7 +185,7 @@ class TestNodes(unittest.TestCase):
# existing nodes remain unchanged
self.assertSetEqual(self.ent_ids, nodes._ensure_nodes(self.ent_type, self.ent_ids))
- self.assertSetEqual(set(self.backend._graph), {
+ self.assertSetEqual(set(self.backend._graph), self.schema_triples | {
# entity definitions
(rdflib.URIRef('http://example.com/me/entity#1234'), rdflib.RDF.type, rdflib.URIRef('http://bsfs.ai/schema/Entity')),
(rdflib.URIRef('http://example.com/me/entity#4321'), rdflib.RDF.type, rdflib.URIRef('http://bsfs.ai/schema/Entity')),
@@ -186,7 +200,7 @@ class TestNodes(unittest.TestCase):
time_triples = list(self.backend._graph.objects(rdflib.URIRef('http://example.com/me/tag#1234'), rdflib.URIRef(self.t_created.uri)))
t_tag_created = float(time_triples[0]) if len(time_triples) > 0 else 0.0
# check triples
- self.assertSetEqual(set(self.backend._graph), {
+ self.assertSetEqual(set(self.backend._graph), self.schema_triples | {
# previous triples
(rdflib.URIRef('http://example.com/me/entity#1234'), rdflib.RDF.type, rdflib.URIRef('http://bsfs.ai/schema/Entity')),
(rdflib.URIRef('http://example.com/me/entity#4321'), rdflib.RDF.type, rdflib.URIRef('http://bsfs.ai/schema/Entity')),
@@ -202,7 +216,7 @@ class TestNodes(unittest.TestCase):
def test___set(self):
# setup
nodes = Nodes(self.backend, self.user, self.ent_type, {URI('http://example.com/me/entity#1234'), URI('http://example.com/me/entity#4321')})
- self.assertSetEqual(set(self.backend._graph), set())
+ self.assertSetEqual(set(self.backend._graph), self.schema_triples | set())
set_ = nodes._Nodes__set
# node_type must match predicate's domain
@@ -217,7 +231,7 @@ class TestNodes(unittest.TestCase):
time_triples = list(self.backend._graph.objects(rdflib.URIRef('http://example.com/me/entity#1234'), rdflib.URIRef(self.t_created.uri)))
t_ent_created = float(time_triples[0]) if len(time_triples) > 0 else 0.0
# verify triples
- self.assertSetEqual(set(self.backend._graph), {
+ self.assertSetEqual(set(self.backend._graph), self.schema_triples | {
# entity definitions
(rdflib.URIRef('http://example.com/me/entity#1234'), rdflib.RDF.type, rdflib.URIRef('http://bsfs.ai/schema/Entity')),
(rdflib.URIRef('http://example.com/me/entity#4321'), rdflib.RDF.type, rdflib.URIRef('http://bsfs.ai/schema/Entity')),
@@ -236,7 +250,7 @@ class TestNodes(unittest.TestCase):
time_triples = list(self.backend._graph.objects(rdflib.URIRef('http://example.com/me/tag#1234'), rdflib.URIRef(self.t_created.uri)))
t_tag_created = float(time_triples[0]) if len(time_triples) > 0 else 0.0
# verify triples
- self.assertSetEqual(set(self.backend._graph), {
+ self.assertSetEqual(set(self.backend._graph), self.schema_triples | {
# previous values
(rdflib.URIRef('http://example.com/me/entity#1234'), rdflib.RDF.type, rdflib.URIRef('http://bsfs.ai/schema/Entity')),
(rdflib.URIRef('http://example.com/me/entity#4321'), rdflib.RDF.type, rdflib.URIRef('http://bsfs.ai/schema/Entity')),
@@ -265,7 +279,7 @@ class TestNodes(unittest.TestCase):
Nodes(self.backend, self.user, self.ent_type, self.ent_ids))
def test_set(self):
- self.assertSetEqual(set(self.backend._graph), set())
+ self.assertSetEqual(set(self.backend._graph), self.schema_triples | set())
nodes = Nodes(self.backend, self.user, self.ent_type, self.ent_ids)
# can set literal values
self.assertEqual(nodes, nodes.set(self.p_filesize.uri, 1234))
@@ -312,7 +326,7 @@ class TestNodes(unittest.TestCase):
def test_set_from_iterable(self):
- self.assertSetEqual(set(self.backend._graph), set())
+ self.assertSetEqual(set(self.backend._graph), self.schema_triples | set())
nodes = Nodes(self.backend, self.user, self.ent_type, self.ent_ids)
# can set literal and node values simultaneously
self.assertEqual(nodes, nodes.set_from_iterable({
diff --git a/test/graph/test_resolve.py b/test/graph/test_resolve.py
new file mode 100644
index 0000000..5bc99e4
--- /dev/null
+++ b/test/graph/test_resolve.py
@@ -0,0 +1,181 @@
+"""
+
+Part of the bsfs test suite.
+A copy of the license is provided with the project.
+Author: Matthias Baumgartner, 2022
+"""
+# imports
+import unittest
+
+# bsie imports
+from bsfs import schema as bsc
+from bsfs.graph import Graph, nodes
+from bsfs.namespace import ns
+from bsfs.query import ast
+from bsfs.triple_store import SparqlStore
+from bsfs.utils import URI, errors
+
+# objects to test
+from bsfs.graph.resolve import Filter
+
+
+## code ##
+
+class TestFilter(unittest.TestCase):
+ """
+
+ NOTE: The Filter resolver is relatively simple as it only checks and changes
+ ast.filter.Is instances. Hence, we don't test all methods individually but
+ all of them with respect to ast.filter.Is elements.
+
+ """
+
+ def test_call(self):
+ schema = bsc.Schema.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:Tag rdfs:subClassOf bsfs:Node .
+ xsd:string rdfs:subClassOf bsfs:Literal .
+ xsd:integer rdfs:subClassOf bsfs:Literal .
+
+ bse:comment rdfs:subClassOf bsfs:Predicate ;
+ rdfs:domain bsfs:Entity ;
+ rdfs:range xsd:string ;
+ bsfs:unique "false"^^xsd:boolean .
+
+ bse:filesize rdfs:subClassOf bsfs:Predicate ;
+ rdfs:domain bsfs:Entity ;
+ rdfs:range xsd:integer ;
+ bsfs:unique "false"^^xsd:boolean .
+
+ bse:tag rdfs:subClassOf bsfs:Predicate ;
+ rdfs:domain bsfs:Entity ;
+ rdfs:range bsfs:Tag ;
+ bsfs:unique "false"^^xsd:boolean .
+ ''')
+ backend = SparqlStore.Open()
+ backend.schema = schema
+ graph = Graph(backend, URI('http://example.com/me'))
+ ents = graph.nodes(ns.bsfs.Entity,
+ {URI('http://example.com/me/entity#1234'), URI('http://example.com/me/entity#4321')})
+ tags = graph.nodes(ns.bsfs.Tag,
+ {URI('http://example.com/me/tag#1234'), URI('http://example.com/me/tag#4321')})
+ invalid = nodes.Nodes(None, '', schema.node(ns.bsfs.Node).get_child(ns.bsfs.Invalid),
+ {'http://example.com/you/invalid#1234', 'http://example.com/you/invalid#4321'})
+ resolver = Filter(schema)
+
+ # immediate Is
+ self.assertEqual(resolver(schema.node(ns.bsfs.Entity),
+ ast.filter.Is(ents)),
+ ast.filter.Or(
+ ast.filter.Is('http://example.com/me/entity#1234'),
+ ast.filter.Is('http://example.com/me/entity#4321')
+ ))
+ # only resolves nodes instances, not URIs
+ self.assertEqual(resolver(schema.node(ns.bsfs.Entity),
+ ast.filter.Is('http://example.com/me/entity#1234')),
+ ast.filter.Is('http://example.com/me/entity#1234'))
+ self.assertEqual(resolver(schema.node(ns.bsfs.Entity),
+ ast.filter.Is(1234)),
+ ast.filter.Is(1234))
+
+ # within And (also checks _value)
+ self.assertEqual(resolver(schema.node(ns.bsfs.Entity),
+ ast.filter.And(
+ ast.filter.Is(ents),
+ ast.filter.Any(ns.bse.comment, ast.filter.Equals('hello world')),
+ )),
+ ast.filter.And(
+ ast.filter.Or(
+ ast.filter.Is('http://example.com/me/entity#1234'),
+ ast.filter.Is('http://example.com/me/entity#4321')),
+ ast.filter.Any(ns.bse.comment, ast.filter.Equals('hello world'))
+ ))
+ # within Or (checks _bounded)
+ self.assertEqual(resolver(schema.node(ns.bsfs.Entity),
+ ast.filter.Or(
+ ast.filter.Is(ents),
+ ast.filter.Any(ns.bse.filesize, ast.filter.LessThan(5)),
+ )),
+ ast.filter.Or(
+ ast.filter.Or(
+ ast.filter.Is('http://example.com/me/entity#1234'),
+ ast.filter.Is('http://example.com/me/entity#4321')),
+ ast.filter.Any(ns.bse.filesize, ast.filter.LessThan(5))
+ ))
+
+ # Any-branched Is
+ self.assertEqual(resolver(schema.node(ns.bsfs.Entity),
+ ast.filter.Any(ns.bse.tag, ast.filter.Is(tags))),
+ ast.filter.Any(ns.bse.tag, ast.filter.Or(
+ ast.filter.Is('http://example.com/me/tag#1234'),
+ ast.filter.Is('http://example.com/me/tag#4321')),
+ ))
+ # All-branched Is
+ self.assertEqual(resolver(schema.node(ns.bsfs.Entity),
+ ast.filter.All(ns.bse.tag, ast.filter.Is(tags))),
+ ast.filter.All(ns.bse.tag, ast.filter.Or(
+ ast.filter.Is('http://example.com/me/tag#1234'),
+ ast.filter.Is('http://example.com/me/tag#4321')),
+ ))
+ # Negated predicate
+ self.assertEqual(resolver(schema.node(ns.bsfs.Tag),
+ ast.filter.Any(ast.filter.Predicate(ns.bse.tag, reverse=True), ast.filter.Is(ents))),
+ ast.filter.Any(ast.filter.Predicate(ns.bse.tag, reverse=True), ast.filter.Or(
+ ast.filter.Is('http://example.com/me/entity#1234'),
+ ast.filter.Is('http://example.com/me/entity#4321')),
+ ))
+
+ # negated Is
+ self.assertEqual(resolver(schema.node(ns.bsfs.Entity),
+ ast.filter.Not(ast.filter.Is(ents))),
+ ast.filter.Not(
+ ast.filter.Or(
+ ast.filter.Is('http://example.com/me/entity#1234'),
+ ast.filter.Is('http://example.com/me/entity#4321')),
+ ))
+
+ # for sake of completeness: Has
+ self.assertEqual(resolver(schema.node(ns.bsfs.Entity),
+ ast.filter.Has(ns.bse.comment)),
+ ast.filter.Has(ns.bse.comment))
+ # route errors
+ self.assertRaises(errors.BackendError, resolver, schema.node(ns.bsfs.Tag),
+ ast.filter.Predicate(ns.bse.comment))
+ self.assertRaises(errors.BackendError, resolver, schema.node(ns.bsfs.Tag),
+ ast.filter.Any(ast.filter.PredicateExpression(), ast.filter.Equals('foo')))
+ self.assertRaises(errors.UnreachableError, resolver._one_of, ast.filter.OneOf(ast.filter.Predicate(ns.bsfs.Predicate)))
+
+ # check schema consistency
+ self.assertRaises(errors.ConsistencyError, resolver, schema.node(ns.bsfs.Tag),
+ ast.filter.Is(invalid))
+ # check immediate type compatibility
+ self.assertRaises(errors.ConsistencyError, resolver, schema.node(ns.bsfs.Tag),
+ ast.filter.Is(ents))
+ self.assertRaises(errors.ConsistencyError, resolver, schema.node(ns.bsfs.Entity),
+ ast.filter.Is(tags))
+ # check type compatibility through branches
+ self.assertRaises(errors.ConsistencyError, resolver, schema.node(ns.bsfs.Tag),
+ ast.filter.Any(ns.bse.comment, ast.filter.Is(tags)))
+ self.assertRaises(errors.ConsistencyError, resolver, schema.node(ns.bsfs.Tag),
+ ast.filter.Any(ns.bse.invalid, ast.filter.Is(tags)))
+ self.assertRaises(errors.ConsistencyError, resolver, schema.node(ns.bsfs.Tag),
+ ast.filter.Any(ast.filter.OneOf(ns.bse.comment, ns.bse.tag), ast.filter.Is(tags)))
+ self.assertRaises(errors.ConsistencyError, resolver, schema.node(ns.bsfs.Tag),
+ ast.filter.Any(ast.filter.OneOf(ns.bse.comment, ns.bse.filesize), ast.filter.Is(tags)))
+ self.assertRaises(errors.ConsistencyError, resolver, schema.node(ns.bsfs.Tag),
+ ast.filter.Any(ast.filter.Predicate(ns.bse.tag, reverse=True), ast.filter.Is(tags)))
+
+
+
+## main ##
+
+if __name__ == '__main__':
+ unittest.main()
+
+## EOF ##