[CWEP002] Add schema finalization checks for computed relations (rules)
authorLea Capgen <lea.capgen@logilab.fr>, Sylvain Thénault <sylvain.thenault@logilab.fr>, Denis Laxalde <denis.laxalde@logilab.fr>
Fri, 12 Sep 2014 14:46:11 +0200
changeset 9952 0f3f965b6365
parent 9951 8cdcbf3f4fd0
child 9953 643b19d79e4a
[CWEP002] Add schema finalization checks for computed relations (rules) Related to #3546717
schema.py
test/unittest_schema.py
--- a/schema.py	Fri Sep 12 14:10:03 2014 +0200
+++ b/schema.py	Fri Sep 12 14:46:11 2014 +0200
@@ -41,6 +41,7 @@
                          fill_schema_from_namespace)
 
 from rql import parse, nodes, RQLSyntaxError, TypeResolverException
+from rql.analyze import ETypeResolver
 
 import cubicweb
 from cubicweb import ETYPE_NAME_MAP, ValidationError, Unauthorized
@@ -1007,6 +1008,35 @@
     def schema_by_eid(self, eid):
         return self._eid_index[eid]
 
+    def iter_computed_relations(self):
+        for relation in self.relations():
+            if relation.rule:
+                yield relation
+
+    def finalize(self):
+        super(CubicWebSchema, self).finalize()
+        self.finalize_computed_relations()
+
+    def finalize_computed_relations(self):
+        """Build relation definitions for computed relations
+
+        The subject and object types are infered using rql analyzer.
+        """
+        analyzer = ETypeResolver(self)
+        for rschema in self.iter_computed_relations():
+            # XXX rule is valid if both S and O are defined and not in an exists
+            rqlexpr = RRQLExpression(rschema.rule)
+            rqlst = rqlexpr.snippet_rqlst
+            analyzer.visit(rqlst)
+            couples = set((sol['S'], sol['O']) for sol in rqlst.solutions)
+            for subjtype, objtype in couples:
+                if self[objtype].final:
+                    raise BadSchemaDefinition('computed relations cannot be final')
+                rdef = ybo.RelationDefinition(
+                    subjtype, rschema.type, objtype)
+                rdef.infered = True
+                self.add_relation_def(rdef)
+
 
 # additional cw specific constraints ###########################################
 
--- a/test/unittest_schema.py	Fri Sep 12 14:10:03 2014 +0200
+++ b/test/unittest_schema.py	Fri Sep 12 14:46:11 2014 +0200
@@ -26,14 +26,16 @@
 
 from yams import ValidationError, BadSchemaDefinition
 from yams.constraints import SizeConstraint, StaticVocabularyConstraint
-from yams.buildobjs import RelationDefinition, EntityType, RelationType
+from yams.buildobjs import (RelationDefinition, EntityType, RelationType,
+                            String, SubjectRelation, ComputedRelation)
 from yams.reader import fill_schema
 
 from cubicweb.schema import (
     CubicWebSchema, CubicWebEntitySchema, CubicWebSchemaLoader,
     RQLConstraint, RQLUniqueConstraint, RQLVocabularyConstraint,
     RQLExpression, ERQLExpression, RRQLExpression,
-    normalize_expression, order_eschemas, guess_rrqlexpr_mainvars)
+    normalize_expression, order_eschemas, guess_rrqlexpr_mainvars,
+    build_schema_from_namespace)
 from cubicweb.devtools import TestServerConfiguration as TestConfiguration
 from cubicweb.devtools.testlib import CubicWebTC
 
@@ -282,6 +284,61 @@
                           'delete': ('managers',)})
 
 
+class SchemaReaderComputedRelationAndAttributesTest(TestCase):
+
+    def test_infer_computed_relation(self):
+        class Person(EntityType):
+            name = String()
+
+        class Company(EntityType):
+            name  = String()
+
+        class Service(EntityType):
+            name = String()
+
+        class works_for(RelationDefinition):
+            subject = 'Person'
+            object = 'Company'
+
+        class produce(RelationDefinition):
+            subject = ('Person', 'Company')
+            object = 'Service'
+
+        class achete(RelationDefinition):
+            subject = 'Person'
+            object = 'Service'
+
+        class produces_and_buys(ComputedRelation):
+            rule = 'S produce O, S achete O'
+
+        class produces_and_buys2(ComputedRelation):
+            rule = 'S works_for SO, SO produce O'
+
+        class reproduce(ComputedRelation):
+            rule = 'S produce O'
+
+        schema = build_schema_from_namespace(vars().items())
+
+        # check object/subject type
+        self.assertEqual([('Person','Service')],
+                         schema['produces_and_buys'].rdefs.keys())
+        self.assertEqual([('Person','Service')],
+                         schema['produces_and_buys2'].rdefs.keys())
+        self.assertEqual([('Company', 'Service'), ('Person', 'Service')],
+                         schema['reproduce'].rdefs.keys())
+        # check relations as marked infered
+        self.assertTrue(
+            schema['produces_and_buys'].rdefs[('Person','Service')].infered)
+
+        del schema
+        class autoname(ComputedRelation):
+            rule = 'S produce X, X name O'
+
+        with self.assertRaises(BadSchemaDefinition) as cm:
+            build_schema_from_namespace(vars().items())
+        self.assertEqual(str(cm.exception), 'computed relations cannot be final')
+
+
 class BadSchemaTC(TestCase):
     def setUp(self):
         self.loader = CubicWebSchemaLoader()