Major Refactoring on plyxproto

- Reorganized code and removed copy of ply
- Reformatted code to be closer to PEP8
- Updated to Python3 compatibility
- Added tox test runner and created test cases
diff --git a/tests/__init__.py b/tests/__init__.py
new file mode 100644
index 0000000..e42366d
--- /dev/null
+++ b/tests/__init__.py
@@ -0,0 +1,13 @@
+# Copyright 2017-present Open Networking Foundation and others
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
diff --git a/tests/test_logicparser.py b/tests/test_logicparser.py
new file mode 100644
index 0000000..bb3107d
--- /dev/null
+++ b/tests/test_logicparser.py
@@ -0,0 +1,72 @@
+# Copyright 2017-present Open Networking Foundation
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+from __future__ import print_function
+import unittest
+
+import ply.lex as lex
+import ply.yacc as yacc
+from plyxproto.logicparser import FOLLexer, FOLParser
+
+
+class TestFOL(unittest.TestCase):
+
+    @classmethod
+    def setUpClass(self):
+
+        self.DEBUG = 0
+        self.lexer = lex.lex(module=FOLLexer(), debug=self.DEBUG)
+        self.parser = yacc.yacc(module=FOLParser(), start='goal', debug=self.DEBUG)
+
+    def test_true(self):
+        '''verify basic truth statement'''
+
+        t_in = "<true>"
+        p_out = self.parser.parse(t_in, lexer=self.lexer, debug=self.DEBUG)
+        self.assertEqual(p_out, "true")
+
+    def test_or(self):
+        '''verify or statement'''
+
+        t_in = "<a | b>"
+        p_out = self.parser.parse(t_in, lexer=self.lexer, debug=self.DEBUG)
+        self.assertEqual(p_out, {'|': ['a', 'b']})
+
+    def test_exists(self):
+        '''verify exists statement'''
+
+        t_in = "<exists a: x=y>"
+        p_out = self.parser.parse(t_in, lexer=self.lexer, debug=self.DEBUG)
+        self.assertEqual(p_out, {'exists': ['a', {'=': ('x', 'y')}]})
+
+    def test_forall(self):
+        '''verify forall statement'''
+
+        t_in = "<forall a: exists b: x.b=y.b>"
+        p_out = self.parser.parse(t_in, lexer=self.lexer, debug=self.DEBUG)
+        self.assertEqual(p_out, {'forall': ['a', {'exists': ['b', {'=': ('x.b', 'y.b')}]}]})
+
+    def test_endswith(self):
+        '''verify endswith statement'''
+
+        t_in = "<forall a: {{ a.endswith('good') }}>"
+        p_out = self.parser.parse(t_in, lexer=self.lexer, debug=self.DEBUG)
+        self.assertEqual(p_out, {'forall': ['a', {'python': "a.endswith('good')"}]})
+
+    def test_function(self):
+        '''verify policy function calls'''
+
+        t_in = "< *doit(foo) >"
+        p_out = self.parser.parse(t_in, lexer=self.lexer, debug=self.DEBUG)
+        self.assertEqual(p_out, {'policy': ['doit', 'foo']})
diff --git a/tests/test_parser.py b/tests/test_parser.py
new file mode 100644
index 0000000..fc7d8c7
--- /dev/null
+++ b/tests/test_parser.py
@@ -0,0 +1,431 @@
+# Copyright 2017-present Open Networking Foundation
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+from __future__ import print_function
+import unittest
+
+import plyxproto.parser as plyxparser
+import plyxproto.model as plyxmodel
+import plyxproto.helpers as plyxhelpers
+
+# Example of creating unittests on PLY code:
+#  https://github.com/dabeaz/ply/tree/master/test
+#  http://www.dalkescientific.com/writings/NBN/parsing_with_ply.html
+
+
+def get_name(element):
+    '''returns name of element'''
+    return element.name.value.pval
+
+
+def get_field_type(ft):
+    ''' returns field type'''
+
+    # FIXME: these are confusing
+    if isinstance(ft, plyxmodel.FieldType):
+        return ft.name.pval
+
+    elif isinstance(ft, plyxmodel.DotName):
+        return ft.value
+
+    else:
+        print("unknown field type: %s" % ft)
+        raise BaseException
+
+
+def get_field_directives(field):
+    ''' navigate the morass that is fieldDirective '''
+
+    fds = {}
+
+    for fieldd in field.fieldDirective:
+
+        fd_key = ""
+        fd_val = ""
+
+        # FIXME: Do these differences in heirarchy have value? They seem
+        # arbitrary and maybe the parser should handle it instead?
+
+        if hasattr(fieldd, 'name'):
+            fd_key = fieldd.name.value
+        elif hasattr(fieldd.pval.name, 'pval'):
+            fd_key = fieldd.pval.name.pval
+        elif hasattr(fieldd.pval.name.value, 'pval'):
+            fd_key = fieldd.pval.name.value.pval
+        else:
+            print("problem with key in fieldDirective: ", fieldd)
+            raise BaseException
+
+        if type(fieldd) is list:
+            fd_val = [item.pval for item in fieldd.value]
+        elif hasattr(fieldd, 'value'):
+            if type(fieldd.value) is list:
+                fd_val = [item.pval for item in fieldd.value]
+            elif hasattr(fieldd.value, 'pval'):
+                fd_val = fieldd.value.pval
+            elif hasattr(fieldd.value, 'value'):
+                if hasattr(fieldd.value.value, 'pval'):
+                    fd_val = fieldd.value.value.pval
+                else:
+                    fd_val = fieldd.value.value
+            else:
+                print("problem with value.value in fieldDirective: %s" % fieldd)
+                raise BaseException
+        elif hasattr(fieldd.pval.value, 'pval'):
+            fd_val = fieldd.pval.value.pval
+        elif hasattr(fieldd.pval.value.value, 'pval'):
+            fd_val = fieldd.pval.value.value.pval
+        else:
+            print("problem with value in fieldDirective: ", fieldd)
+            raise BaseException
+
+        fds[fd_key] = fd_val
+
+    return fds
+
+
+def msg_dict(msgdef):
+    '''
+    Given a MessageDefinition object, returns a dict describing it
+    Should probably really be a recursive __dict__ method on that object
+    '''
+
+    fd = {}  # FieldDefinition
+    md = {}  # sub-ModelDefinition
+    en = {}  # EnumDefinitons
+    ed = {}  # ExtensionsDirective
+    os = {}  # OptionStatement
+
+    assert isinstance(msgdef, plyxmodel.MessageDefinition)
+
+    for field in msgdef.body:
+
+        if isinstance(field, plyxmodel.FieldDefinition):
+
+            fd[field.name.value.pval] = {
+                'id': int(field.fieldId.pval),
+                'modifier': field.field_modifier.pval,
+                'type': get_field_type(field.ftype),
+                'policy': field.policy,
+                'directives': get_field_directives(field),
+            }
+
+        elif isinstance(field, plyxmodel.LinkSpec):
+
+            fd[field.field_def.name.value.pval] = {
+                'id': int(field.field_def.fieldId.pval),
+                'modifier': field.field_def.field_modifier.pval,
+                'type': field.field_def.ftype.value,  # Why different (not in a LU as in FieldDefinition)?
+                'link_type': field.link_def.link_type.pval,
+                'link_name': field.link_def.name[0].pval,  # Why in a list?
+                'link_src_port': field.link_def.src_port.value.pval,  # confusingly named - why include "port" ?
+                'link_dst_port': field.link_def.dst_port.value.pval,
+                'directives': get_field_directives(field.field_def),
+            }
+
+        elif isinstance(field, plyxmodel.MessageDefinition):
+
+            md[field.name.value.pval] = msg_dict(field)
+
+        elif isinstance(field, plyxmodel.EnumDefinition):
+
+            enumopts = {}
+
+            for enumdef in field.body:
+                enumopts[int(enumdef.fieldId.pval)] = get_name(enumdef)
+
+            en[field.name.value.pval] = enumopts
+
+        elif isinstance(field, plyxmodel.ExtensionsDirective):
+
+            # FIXME: models.ExtensionsMax() isn't well defined, so
+            # omit to/from when non-LU is found
+
+            if type(field.fromVal) is plyxhelpers.LU:
+                ed['from'] = field.fromVal.pval
+
+            if type(field.toVal) is plyxhelpers.LU:
+                ed['to'] = field.toVal.pval
+
+        elif isinstance(field, plyxmodel.OptionStatement):
+
+            os[field.name.value.pval] = field.value.value.pval
+
+        else:
+            print("Unknown message type: %s" % type(field))
+            raise BaseException
+
+    return fd, md, en, ed, os
+
+
+def options_dict(protofile):
+    '''returns a dictionary of name:value options from a ProtoFile'''
+
+    options = {}
+
+    for item in protofile.body:
+        if isinstance(item, plyxmodel.OptionStatement):
+            options[item.name.value.pval] = item.value.value.pval
+
+    return options
+
+
+class TestParser(unittest.TestCase):
+
+    @classmethod
+    def setUpClass(self):
+        self.parser = plyxparser.ProtobufAnalyzer()
+
+    def test_invalid_input(self):
+        '''invalid input'''
+
+        t_in = """this is invalid"""
+
+        with self.assertRaises(plyxparser.ParsingError):
+            self.parser.parse_string(t_in)
+
+    def test_package(self):
+        '''creating a package'''
+
+        t_in = """package tutorial;"""
+        p_out = self.parser.parse_string(t_in)
+
+        self.assertIsInstance(p_out, plyxmodel.ProtoFile)
+        self.assertIsInstance(p_out.body[0], plyxmodel.PackageStatement)
+        self.assertEqual(p_out.body[0].name.value[0].pval, "tutorial")
+
+    def test_def_policy(self):
+        '''defining a policy'''
+
+        t_in = """policy foo <exists foo: foo.x=foo.y>"""
+        p_out = self.parser.parse_string(t_in)
+
+        self.assertIsInstance(p_out, plyxmodel.ProtoFile)
+        self.assertIsInstance(p_out.body[0], plyxmodel.PolicyDefinition)
+        self.assertEqual(get_name(p_out.body[0]), "foo")
+        self.assertDictEqual(p_out.body[0].body, {'exists': ['foo', {'=': ('foo.x', 'foo.y')}]})
+
+    def test_def_message(self):
+        '''defining a message'''
+
+        t_in = """package tutorial;
+
+message Person {
+  required string name = 1;
+  required int32 id = 2;
+  optional string email = 3;
+}
+"""
+        p_out = self.parser.parse_string(t_in)
+
+        self.assertIsInstance(p_out, plyxmodel.ProtoFile)
+        # see test_package for testing `package ...` statement
+        self.assertIsInstance(p_out.body[1], plyxmodel.MessageDefinition)
+        self.assertEqual(get_name(p_out.body[1]), 'Person')
+
+        fd, md, en, ed, os = msg_dict(p_out.body[1])
+
+        self.assertDictEqual(fd['name'],
+                             {'id': 1, 'modifier': 'required', 'type': 'string', 'policy': None, 'directives': {}})
+        self.assertDictEqual(fd['id'],
+                             {'id': 2, 'modifier': 'required', 'type': 'int32', 'policy': None, 'directives': {}})
+        self.assertDictEqual(fd['email'],
+                             {'id': 3, 'modifier': 'optional', 'type': 'string', 'policy': None, 'directives': {}})
+
+    def test_options(self):
+        '''setting options'''
+
+        t_in = """package tutorial;
+option java_outer_classname = "PushNotifications";
+option optimize_for = SPEED;
+"""
+        p_out = self.parser.parse_string(t_in)
+        od = options_dict(p_out)
+
+        self.assertDictEqual(od, {'java_outer_classname': '"PushNotifications"', 'optimize_for': 'SPEED'})
+
+    def test_def_relation(self):
+        '''test defining related messages'''
+
+        t_in = """package tutorial;
+
+message Person(core.Actor) {
+  required string name = 1;
+  required int32 id = 2;
+  optional string email = 3;
+
+  required manytoone work_location->Location/types.Company:employees = 4;
+
+  enum PhoneType {
+    MOBILE = 0;
+    HOME = 1;
+    WORK = 2;
+  }
+
+  message PhoneNumber {
+    required string number = 1;
+    optional PhoneType type = 2 [default = HOME];
+  }
+
+  repeated PhoneNumber phone = 4;
+  extensions 500 to 990;
+}
+
+message AddressBook {
+  repeated Person person = 1;
+
+  // Possible extension numbers.
+  extensions 500 to max;
+}
+"""
+        p_out = self.parser.parse_string(t_in)
+
+        fd, md, en, ed, os = msg_dict(p_out.body[1])
+
+        self.assertDictEqual(fd, {'name': {'id': 1, 'modifier': 'required', 'type': 'string', 'policy': None, 'directives': {}}, 'id': {'id': 2, 'modifier': 'required', 'type': 'int32', 'policy': None, 'directives': {}}, 'email': {'id': 3, 'modifier': 'optional', 'type': 'string', 'policy': None, 'directives': {}}, 'work_location': {'id': 4, 'modifier': 'required', 'type': 'int32', 'link_type': 'manytoone', 'link_name': 'Location', 'link_src_port': 'work_location', 'link_dst_port': 'employees', 'directives': {'type': 'link', 'model': ['Location'], 'port': 'employees'}}, 'phone': {'id': 4, 'modifier': 'repeated', 'type': 'PhoneNumber', 'policy': None, 'directives': {}}})
+
+        self.assertDictEqual(md, {'PhoneNumber': ({'number': {'id': 1, 'modifier': 'required', 'type': 'string', 'policy': None, 'directives': {}}, 'type': {'id': 2, 'modifier': 'optional', 'type': 'PhoneType', 'policy': None, 'directives': {'default': 'HOME'}}}, {}, {}, {}, {})})
+        self.assertDictEqual(en, {'PhoneType': {0: 'MOBILE', 1: 'HOME', 2: 'WORK'}})
+        self.assertDictEqual(ed, {'from': '500', 'to': '990'})
+        self.assertDictEqual(os, {})
+
+        fd, md, en, ed, os = msg_dict(p_out.body[2])
+
+        self.assertDictEqual(fd, {'person': {'id': 1, 'modifier': 'repeated', 'type': 'Person', 'policy': None, 'directives': {}}})
+        self.assertDictEqual(md, {})
+        self.assertDictEqual(en, {})
+        self.assertDictEqual(ed, {'from': '500'})
+        self.assertDictEqual(os, {})
+
+    def test_xos_core(self):
+        '''test chunk of xos core xproto'''
+
+        t_in = """
+option app_label = "core";
+option legacy="True";
+
+// use thi policy to allow access to admins only
+policy admin_policy < ctx.user.is_admin >
+
+message XOSBase {
+     option skip_init = True;
+     option custom_header = "xosbase_header";
+     option abstract = True;
+
+     // field 1 is reserved for "id"
+     required string created = 2 [content_type = "date", auto_now_add = True, help_text = "Time this model was created"];
+     required string updated = 3 [default = "now()", content_type = "date", help_text = "Time this model was changed by a non-synchronizer"];
+     optional string enacted = 4 [null = True, content_type = "date", blank = True, default = None, help_text = "When synced, set to the timestamp of the data that was synced"];
+     optional string policed = 5 [null = True, content_type = "date", blank = True, default = None, help_text = "When policed, set to the timestamp of the data that was policed"];
+     optional string backend_register = 6 [default = "{}", max_length = 1024, feedback_state = True];
+     required bool backend_need_delete = 7 [default = False, blank = True];
+     required bool backend_need_reap = 8 [default = False, blank = True];
+     required string backend_status = 9 [default = "Provisioning in progress", max_length = 1024, null = True, feedback_state = True];
+     required int32 backend_code = 10 [default = 0, feedback_state = True];
+     required bool deleted = 11 [default = False, blank = True];
+     required bool write_protect = 12 [default = False, blank = True];
+     required bool lazy_blocked = 13 [default = False, blank = True];
+     required bool no_sync = 14 [default = False, blank = True];
+     required bool no_policy = 15 [default = False, blank = True];
+     optional string policy_status = 16 [default = "Policy in process", max_length = 1024, feedback_state = True];
+     optional int32 policy_code = 17 [default = 0, feedback_state = True];
+     required string leaf_model_name = 18 [null = False, max_length = 1024, help_text = "The most specialized model in this chain of inheritance, often defined by a service developer"];
+     required bool backend_need_delete_policy = 19 [default = False, help_text = "True if delete model_policy must be run before object can be reaped", blank = True];
+     required bool xos_managed = 20 [default = True, help_text = "True if xos is responsible for creating/deleting this object", blank = True, gui_hidden = True];
+     optional string backend_handle = 21 [max_length = 1024, feedback_state = True, blank=True, null=True, help_text = "Handle used by the backend to track this object", gui_hidden = True];
+     optional string changed_by_step = 22 [null = True, content_type = "date", blank = True, default = None, gui_hidden = True, help_text = "Time this model was changed by a sync step"];
+     optional string changed_by_policy = 23 [null = True, content_type = "date", blank = True, default = None, gui_hidden = True, help_text = "Time this model was changed by a model policy"];
+}
+
+// A user may give a permission that he has to another user
+policy grant_policy < ctx.user.is_admin
+                      | exists Privilege:Privilege.object_type = obj.object_type
+                        & Privilege.object_id = obj.object_id
+                        & Privilege.accessor_type = "User"
+                        & Privilege.accessor_id = ctx.user.id
+                        & Privilege.permission = "role:admin" >
+
+message Privilege::grant_policy (XOSBase) {
+     required int32 accessor_id = 1 [null = False, blank=False];
+     required string accessor_type = 2 [null = False, max_length=1024, blank = False];
+     optional int32 controller_id = 3 [null = True, blank = True];
+     required int32 object_id = 4 [null = False, blank=False];
+     required string object_type = 5 [null = False, max_length=1024, blank = False];
+     required string permission = 6 [null = False, default = "all", max_length=1024, tosca_key=True];
+     required string granted = 7 [content_type = "date", auto_now_add = True, max_length=1024];
+     required string expires = 8 [content_type = "date", null = True, max_length=1024];
+}
+"""
+
+        p_out = self.parser.parse_string(t_in)
+
+        # check options
+        od = options_dict(p_out)
+        self.assertDictEqual(od, {'app_label': '"core"', 'legacy': '"True"'})
+
+        self.assertIsInstance(p_out.body[2], plyxmodel.PolicyDefinition)
+        self.assertEqual(get_name(p_out.body[2]), "admin_policy")
+        self.assertEqual(p_out.body[2].body, 'ctx.user.is_admin')
+
+        fd, md, en, ed, os = msg_dict(p_out.body[3])
+
+        self.assertDictEqual(fd, {'created': {'id': 2, 'modifier': 'required', 'type': 'string', 'policy': None, 'directives': {'content_type': '"date"', 'auto_now_add': 'True', 'help_text': '"Time this model was created"'}}, 'updated': {'id': 3, 'modifier': 'required', 'type': 'string', 'policy': None, 'directives': {'default': '"now()"', 'content_type': '"date"', 'help_text': '"Time this model was changed by a non-synchronizer"'}}, 'enacted': {'id': 4, 'modifier': 'optional', 'type': 'string', 'policy': None, 'directives': {'null': 'True', 'content_type': '"date"', 'blank': 'True', 'default': 'None', 'help_text': '"When synced, set to the timestamp of the data that was synced"'}}, 'policed': {'id': 5, 'modifier': 'optional', 'type': 'string', 'policy': None, 'directives': {'null': 'True', 'content_type': '"date"', 'blank': 'True', 'default': 'None', 'help_text': '"When policed, set to the timestamp of the data that was policed"'}}, 'backend_register': {'id': 6, 'modifier': 'optional', 'type': 'string', 'policy': None, 'directives': {'default': '"{}"', 'max_length': '1024', 'feedback_state': 'True'}}, 'backend_need_delete': {'id': 7, 'modifier': 'required', 'type': 'bool', 'policy': None, 'directives': {'default': 'False', 'blank': 'True'}}, 'backend_need_reap': {'id': 8, 'modifier': 'required', 'type': 'bool', 'policy': None, 'directives': {'default': 'False', 'blank': 'True'}}, 'backend_status': {'id': 9, 'modifier': 'required', 'type': 'string', 'policy': None, 'directives': {'default': '"Provisioning in progress"', 'max_length': '1024', 'null': 'True', 'feedback_state': 'True'}}, 'backend_code': {'id': 10, 'modifier': 'required', 'type': 'int32', 'policy': None, 'directives': {'default': '0', 'feedback_state': 'True'}}, 'deleted': {'id': 11, 'modifier': 'required', 'type': 'bool', 'policy': None, 'directives': {'default': 'False', 'blank': 'True'}}, 'write_protect': {'id': 12, 'modifier': 'required', 'type': 'bool', 'policy': None, 'directives': {'default': 'False', 'blank': 'True'}}, 'lazy_blocked': {'id': 13, 'modifier': 'required', 'type': 'bool', 'policy': None, 'directives': {'default': 'False', 'blank': 'True'}}, 'no_sync': {'id': 14, 'modifier': 'required', 'type': 'bool', 'policy': None, 'directives': {'default': 'False', 'blank': 'True'}}, 'no_policy': {'id': 15, 'modifier': 'required', 'type': 'bool', 'policy': None, 'directives': {'default': 'False', 'blank': 'True'}}, 'policy_status': {'id': 16, 'modifier': 'optional', 'type': 'string', 'policy': None, 'directives': {'default': '"Policy in process"', 'max_length': '1024', 'feedback_state': 'True'}}, 'policy_code': {'id': 17, 'modifier': 'optional', 'type': 'int32', 'policy': None, 'directives': {'default': '0', 'feedback_state': 'True'}}, 'leaf_model_name': {'id': 18, 'modifier': 'required', 'type': 'string', 'policy': None, 'directives': {'null': 'False', 'max_length': '1024', 'help_text': '"The most specialized model in this chain of inheritance, often defined by a service developer"'}}, 'backend_need_delete_policy': {'id': 19, 'modifier': 'required', 'type': 'bool', 'policy': None, 'directives': {'default': 'False', 'help_text': '"True if delete model_policy must be run before object can be reaped"', 'blank': 'True'}}, 'xos_managed': {'id': 20, 'modifier': 'required', 'type': 'bool', 'policy': None, 'directives': {'default': 'True', 'help_text': '"True if xos is responsible for creating/deleting this object"', 'blank': 'True', 'gui_hidden': 'True'}}, 'backend_handle': {'id': 21, 'modifier': 'optional', 'type': 'string', 'policy': None, 'directives': {'max_length': '1024', 'feedback_state': 'True', 'blank': 'True', 'null': 'True', 'help_text': '"Handle used by the backend to track this object"', 'gui_hidden': 'True'}}, 'changed_by_step': {'id': 22, 'modifier': 'optional', 'type': 'string', 'policy': None, 'directives': {'null': 'True', 'content_type': '"date"', 'blank': 'True', 'default': 'None', 'gui_hidden': 'True', 'help_text': '"Time this model was changed by a sync step"'}}, 'changed_by_policy': {'id': 23, 'modifier': 'optional', 'type': 'string', 'policy': None, 'directives': {'null': 'True', 'content_type': '"date"', 'blank': 'True', 'default': 'None', 'gui_hidden': 'True', 'help_text': '"Time this model was changed by a model policy"'}}})
+
+        self.assertDictEqual(md, {})
+        self.assertDictEqual(en, {})
+        self.assertDictEqual(ed, {})
+        self.assertDictEqual(os, {'skip_init': 'True', 'custom_header': '"xosbase_header"', 'abstract': 'True'})
+
+        self.assertIsInstance(p_out.body[4], plyxmodel.PolicyDefinition)
+
+        self.assertEqual(get_name(p_out.body[4]), "grant_policy")
+        self.assertDictEqual(p_out.body[4].body, {'|':
+            ['ctx.user.is_admin', {'&':
+                [{'&': [{'&': [{'&': [{'exists': ['Privilege', {'=': ('Privilege.object_type', 'obj.object_type')}]},
+                               {'=': ('Privilege.object_id', 'obj.object_id')}]},
+                               {'=': ('Privilege.accessor_type', '"User"')}]},
+                               {'=': ('Privilege.accessor_id', 'ctx.user.id')}]},
+                               {'=': ('Privilege.permission', '"role:admin"')}]}]}
+                )
+        fd, md, en, ed, os = msg_dict(p_out.body[5])
+
+        self.assertDictEqual(fd, {'accessor_id': {'id': 1, 'modifier': 'required', 'type': 'int32', 'policy': None, 'directives': {'null': 'False', 'blank': 'False'}}, 'accessor_type': {'id': 2, 'modifier': 'required', 'type': 'string', 'policy': None, 'directives': {'null': 'False', 'max_length': '1024', 'blank': 'False'}}, 'controller_id': {'id': 3, 'modifier': 'optional', 'type': 'int32', 'policy': None, 'directives': {'null': 'True', 'blank': 'True'}}, 'object_id': {'id': 4, 'modifier': 'required', 'type': 'int32', 'policy': None, 'directives': {'null': 'False', 'blank': 'False'}}, 'object_type': {'id': 5, 'modifier': 'required', 'type': 'string', 'policy': None, 'directives': {'null': 'False', 'max_length': '1024', 'blank': 'False'}}, 'permission': {'id': 6, 'modifier': 'required', 'type': 'string', 'policy': None, 'directives': {'null': 'False', 'default': '"all"', 'max_length': '1024', 'tosca_key': 'True'}}, 'granted': {'id': 7, 'modifier': 'required', 'type': 'string', 'policy': None, 'directives': {'content_type': '"date"', 'auto_now_add': 'True', 'max_length': '1024'}}, 'expires': {'id': 8, 'modifier': 'required', 'type': 'string', 'policy': None, 'directives': {'content_type': '"date"', 'null': 'True', 'max_length': '1024'}}})
+
+        self.assertDictEqual(md, {})
+        self.assertDictEqual(en, {})
+        self.assertDictEqual(ed, {})
+        self.assertDictEqual(os, {})
+
+# FIXME: these sorts of simple validations should fail but currently don't
+#
+#       def test_invalid_message(self):
+#           '''an invalid message'''
+#
+#           t_in = """message BadMessage {
+#     required string name = 0;
+#     required int32 id = -2;
+#   }"""
+#           p_out = self.parser.parse_string(t_in)
+#           print(p_out)
+#
+#
+#       def test_duplicate_id(self):
+#           '''duplicate id's in a message'''
+#
+#           t_in = """message BadMessage2 {
+#     required string name = 1;
+#     required int32 id = 1;
+#   }"""
+#           p_out = self.parser.parse_string(t_in)
+#           print(p_out)