From 4dc9ecaa264c6d78adc6fda92cf60cc5501cddf1 Mon Sep 17 00:00:00 2001 From: Kaitlyn Johnson Date: Fri, 31 Mar 2017 15:45:38 -0700 Subject: [PATCH 1/9] Add new declarative serializer and corresponding tests. --- sqlalchemy_jsonapi/declarative/__init__.py | 0 sqlalchemy_jsonapi/declarative/serializer.py | 82 +++++++ .../unittests/test_declarative_serializer.py | 214 ++++++++++++++++++ 3 files changed, 296 insertions(+) create mode 100644 sqlalchemy_jsonapi/declarative/__init__.py create mode 100644 sqlalchemy_jsonapi/declarative/serializer.py create mode 100644 sqlalchemy_jsonapi/unittests/test_declarative_serializer.py diff --git a/sqlalchemy_jsonapi/declarative/__init__.py b/sqlalchemy_jsonapi/declarative/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/sqlalchemy_jsonapi/declarative/serializer.py b/sqlalchemy_jsonapi/declarative/serializer.py new file mode 100644 index 0000000..a5f363e --- /dev/null +++ b/sqlalchemy_jsonapi/declarative/serializer.py @@ -0,0 +1,82 @@ +"""A serializer for serializing SQLAlchemy models to JSON API spec.""" + + +class JSONAPISerializer(object): + """A JSON API serializer that serializes SQLAlchemy models.""" + + def serialize(self, resources): + """Serialize resource(s) according to JSON API spec. + + Args: + resources: SQLAlchemy model or SQLAlchemy query + """ + if 'id' not in self.fields: + raise AttributeError + + serialized_object = { + 'included': [], + 'jsonapi': { + 'version': '1.0' + }, + 'meta': { + 'sqlalchemy_jsonapi_version': '4.0.9' + } + } + if isinstance(resources, self.model): + serialized_object['data'] = self._render_resource(resources) + else: + serialized_object['data'] = [] + for resource in resources: + serialized_object['data'].append( + self._render_resource(resource)) + + return serialized_object + + def _render_resource(self, resource): + """Renders a resource's top level members based on JSON API spec. + + Top level members include: + 'id', 'type', 'attributes', 'relationships' + """ + # Must not render a resource that has same named attributes as different model. + if not isinstance(resource, self.model): + raise TypeError + + top_level_members = {} + top_level_members['id'] = str(resource.id) + top_level_members['type'] = resource.__tablename__ + top_level_members['attributes'] = self._render_attributes(resource) + top_level_members['relationships'] = self._render_relationships( + resource) + + return top_level_members + + def _render_attributes(self, resource): + """Render the resources's attributes.""" + attributes = {} + related_models = resource.__mapper__.relationships.keys() + for attribute in self.fields: + if attribute in related_models or attribute == 'id': + continue + try: + attributes[attribute] = getattr(resource, attribute) + except AttributeError: + raise + + return attributes + + def _render_relationships(self, resource): + """Render the resource's relationships.""" + relationships = {} + related_models = resource.__mapper__.relationships.keys() + for model in related_models: + relationships[model] = { + 'links': { + 'self': '/{}/{}/relationships/{}'.format( + resource.__tablename__, resource.id, model), + 'related': '/{}/{}/{}'.format( + resource.__tablename__, resource.id, model) + } + } + + return relationships diff --git a/sqlalchemy_jsonapi/unittests/test_declarative_serializer.py b/sqlalchemy_jsonapi/unittests/test_declarative_serializer.py new file mode 100644 index 0000000..fe86a21 --- /dev/null +++ b/sqlalchemy_jsonapi/unittests/test_declarative_serializer.py @@ -0,0 +1,214 @@ +"""Tests for declarative JSONAPISerializer serialize method.""" + +from sqlalchemy_jsonapi.unittests.utils import testcases +from sqlalchemy_jsonapi.unittests import models +from sqlalchemy_jsonapi.declarative import serializer + + +class UserSerializer(serializer.JSONAPISerializer): + """A serializer for the User model.""" + fields = ['id', 'first', 'last', 'username'] + model = models.User + + +class TestDeclarativeSerializer(testcases.SqlalchemyJsonapiTestCase): + """Tests for JSONAPISerializer.serialize.""" + + def test_serialize_a_single_resource_success(self): + """Serialize a single resource successfully. + + A jsonapi compliant object is returned.""" + user = models.User( + first='Sally', last='Smith', + password='password', username='SallySmith1') + self.session.add(user) + self.session.commit() + user = self.session.query(models.User).get(user.id) + + user_serializer = models.UserSerializer() + serialized_data = user_serializer.serialize(user) + + expected = { + 'data': { + 'type': 'users', + 'id': '1', + 'attributes': { + 'first': u'Sally', + 'last': u'Smith', + 'username': u'SallySmith1' + }, + 'relationships': { + 'posts': { + 'links': { + 'related': '/users/1/posts', + 'self': '/users/1/relationships/posts' + } + }, + 'logs': { + 'links': { + 'related': '/users/1/logs', + 'self': '/users/1/relationships/logs' + } + }, + 'comments': { + 'links': { + 'related': '/users/1/comments', + 'self': '/users/1/relationships/comments' + } + } + } + }, + 'included': [], + 'meta': { + 'sqlalchemy_jsonapi_version': '4.0.9' + }, + 'jsonapi': { + 'version': '1.0' + } + } + self.assertEqual(expected, serialized_data) + + def test_serialize_collection_of_resources(self): + """Serialize a collection of resources successfully returns a jsonapi compliant object.""" + for x in range(2): + user = models.User( + first='Sally-{0}'.format(x+1), + last='Joe-{0}'.format(x+1), + username='SallyJoe{0}'.format(x+1), + password='password') + self.session.add(user) + self.session.commit() + collection = self.session.query(models.User) + + user_serializer = models.UserSerializer() + actual = user_serializer.serialize(collection) + + expected = { + 'data': [{ + 'type': 'users', + 'id': '1', + 'attributes': { + 'first': u'Sally-1', + 'last': u'Joe-1', + 'username': u'SallyJoe1' + }, + 'relationships': { + 'posts': { + 'links': { + 'related': '/users/1/posts', + 'self': '/users/1/relationships/posts' + } + }, + 'logs': { + 'links': { + 'related': '/users/1/logs', + 'self': '/users/1/relationships/logs' + } + }, + 'comments': { + 'links': { + 'related': '/users/1/comments', + 'self': '/users/1/relationships/comments' + } + } + } + }, { + 'type': 'users', + 'id': '2', + 'attributes': { + 'first': u'Sally-2', + 'last': u'Joe-2', + 'username': u'SallyJoe2' + }, + 'relationships': { + 'posts': { + 'links': { + 'related': '/users/2/posts', + 'self': '/users/2/relationships/posts' + } + }, + 'logs': { + 'links': { + 'related': '/users/2/logs', + 'self': '/users/2/relationships/logs' + } + }, + 'comments': { + 'links': { + 'related': '/users/2/comments', + 'self': '/users/2/relationships/comments' + } + } + } + }], + 'included': [], + 'meta': { + 'sqlalchemy_jsonapi_version': '4.0.9' + }, + 'jsonapi': { + 'version': '1.0' + } + } + self.assertEqual(expected['data'], sorted(actual['data'])) + self.assertEqual(expected['meta'], actual['meta']) + self.assertEqual(expected['jsonapi'], actual['jsonapi']) + self.assertEqual(expected['included'], actual['included']) + + def test_serialize_resource_with_missing_required_attribute_id(self): + """Serialize resource without required attriute of 'id' raises AttributeError.""" + user = models.User( + first='Sally', last='Smith', + password='password', username='SallySmith1') + self.session.add(user) + self.session.commit() + user = self.session.query(models.User).get(user.id) + + class InvalidUserSerializer(serializer.JSONAPISerializer): + fields = ['first', 'last', 'username'] + model = models.User + + user_serializer = InvalidUserSerializer() + with self.assertRaises(AttributeError): + user_serializer.serialize(user) + + def test_serialize_resource_with_relationships_as_attributes(self): + """Serialize a resource with fields that are relationships raises an AttributeError. + + The serializer should only serialize attributes and not relationships. + Relationships are shown under the resource objects top member 'relationships'. + """ + user = models.User( + first='Sally', last='Smith', + password='password', username='SallySmith1') + self.session.add(user) + blog_post = models.Post( + title='This Is A Title', content='This is the content', + author_id=user.id, author=user) + self.session.add(blog_post) + self.session.commit() + post = self.session.query(models.Post).get(blog_post.id) + + class InvalidBlogPostSerializer(serializer.JSONAPISerializer): + fields = ['title', 'content', 'author', 'comments'] + model = models.Post + + blog_post_serializer = InvalidBlogPostSerializer() + with self.assertRaises(AttributeError): + blog_post_serializer.serialize(post) + + def test_serialize_resource_with_mismatched_resource(self): + """Serialize a resource with mismatched serializer model raises TypeError.""" + user = models.User( + first='Sally', last='Smith', + password='password', username='SallySmith1') + self.session.add(user) + blog_post = models.Post( + title='This Is A Title', content='This is the content', + author_id=user.id, author=user) + self.session.add(blog_post) + self.session.commit() + post = self.session.query(models.Post).get(blog_post.id) + + user_serializer = models.UserSerializer() + with self.assertRaises(TypeError): + user_serializer.serialize(post) From 73206da1256670f433abbfebd7b912f0b00d679f Mon Sep 17 00:00:00 2001 From: Kaitlyn Johnson Date: Fri, 31 Mar 2017 15:49:38 -0700 Subject: [PATCH 2/9] Fix import issue regarding where UserSerializer is coming from. --- sqlalchemy_jsonapi/unittests/test_declarative_serializer.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/sqlalchemy_jsonapi/unittests/test_declarative_serializer.py b/sqlalchemy_jsonapi/unittests/test_declarative_serializer.py index fe86a21..51a5c59 100644 --- a/sqlalchemy_jsonapi/unittests/test_declarative_serializer.py +++ b/sqlalchemy_jsonapi/unittests/test_declarative_serializer.py @@ -25,7 +25,7 @@ def test_serialize_a_single_resource_success(self): self.session.commit() user = self.session.query(models.User).get(user.id) - user_serializer = models.UserSerializer() + user_serializer = UserSerializer() serialized_data = user_serializer.serialize(user) expected = { @@ -80,7 +80,7 @@ def test_serialize_collection_of_resources(self): self.session.commit() collection = self.session.query(models.User) - user_serializer = models.UserSerializer() + user_serializer = UserSerializer() actual = user_serializer.serialize(collection) expected = { @@ -209,6 +209,6 @@ def test_serialize_resource_with_mismatched_resource(self): self.session.commit() post = self.session.query(models.Post).get(blog_post.id) - user_serializer = models.UserSerializer() + user_serializer = UserSerializer() with self.assertRaises(TypeError): user_serializer.serialize(post) From 9ccef961a6d83d7f0264095de8866123afbc92f1 Mon Sep 17 00:00:00 2001 From: Kaitlyn Johnson Date: Fri, 31 Mar 2017 16:19:56 -0700 Subject: [PATCH 3/9] Since appending list in order, do not need to sort. --- sqlalchemy_jsonapi/unittests/test_declarative_serializer.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/sqlalchemy_jsonapi/unittests/test_declarative_serializer.py b/sqlalchemy_jsonapi/unittests/test_declarative_serializer.py index 51a5c59..1e79110 100644 --- a/sqlalchemy_jsonapi/unittests/test_declarative_serializer.py +++ b/sqlalchemy_jsonapi/unittests/test_declarative_serializer.py @@ -149,7 +149,7 @@ def test_serialize_collection_of_resources(self): 'version': '1.0' } } - self.assertEqual(expected['data'], sorted(actual['data'])) + self.assertEqual(expected['data'], actual['data']) self.assertEqual(expected['meta'], actual['meta']) self.assertEqual(expected['jsonapi'], actual['jsonapi']) self.assertEqual(expected['included'], actual['included']) From b0664f30593f1178674f2998810ef3b2e2730267 Mon Sep 17 00:00:00 2001 From: Kaitlyn Johnson Date: Wed, 5 Apr 2017 11:45:18 -0700 Subject: [PATCH 4/9] Change location for declarative serialize tests and add more tests. --- .../unittests/declarative_tests/__init__.py | 0 .../declarative_tests/test_serialize.py | 605 ++++++++++++++++++ .../unittests/test_declarative_serializer.py | 214 ------- 3 files changed, 605 insertions(+), 214 deletions(-) create mode 100644 sqlalchemy_jsonapi/unittests/declarative_tests/__init__.py create mode 100644 sqlalchemy_jsonapi/unittests/declarative_tests/test_serialize.py delete mode 100644 sqlalchemy_jsonapi/unittests/test_declarative_serializer.py diff --git a/sqlalchemy_jsonapi/unittests/declarative_tests/__init__.py b/sqlalchemy_jsonapi/unittests/declarative_tests/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/sqlalchemy_jsonapi/unittests/declarative_tests/test_serialize.py b/sqlalchemy_jsonapi/unittests/declarative_tests/test_serialize.py new file mode 100644 index 0000000..5f6b507 --- /dev/null +++ b/sqlalchemy_jsonapi/unittests/declarative_tests/test_serialize.py @@ -0,0 +1,605 @@ +"""Tests for declarative JSONAPISerializer serialize method.""" + +import unittest +import datetime + +from sqlalchemy import ( + create_engine, Column, String, Integer, ForeignKey, Boolean, DateTime) +from sqlalchemy.ext.declarative import declarative_base +from sqlalchemy.orm import backref, relationship, sessionmaker + +from sqlalchemy_jsonapi.declarative import serializer + + +class SerializeResourcesWithoutRelatedModels(unittest.TestCase): + """Tests for serializing a resource that has no related models.""" + + def setUp(self): + """Configure sqlalchemy and session.""" + self.engine = create_engine('sqlite://') + Session = sessionmaker(bind=self.engine) + self.session = Session() + self.Base = declarative_base() + + class User(self.Base): + __tablename__ = 'users' + id = Column(Integer, primary_key=True) + first_name = Column(String(50), nullable=False) + age = Column(Integer, nullable=False) + username = Column(String(50), unique=True, nullable=False) + is_admin = Column(Boolean, default=False) + date_joined = Column(DateTime) + + self.User = User + self.Base.metadata.create_all(self.engine) + + def tearDown(self): + """Reset the sqlalchemy engine.""" + self.Base.metadata.drop_all(self.engine) + + def test_serialize_single_resource_with_only_id_field(self): + """Serialize a resource with only an 'id' field. + + If attributes, other than 'id', are not specified in fields, + then the attributes remain an empty object. + """ + + class UserSerializer(serializer.JSONAPISerializer): + """Declarative serializer for User.""" + fields = ['id'] + model = self.User + dasherize = True + + user = self.User( + first_name='Sally', age=27, is_admin=True, + username='SallySmith1', date_joined=datetime.date(2017, 12, 5)) + self.session.add(user) + self.session.commit() + user = self.session.query(self.User).get(user.id) + + user_serializer = UserSerializer() + serialized_data = user_serializer.serialize(user) + + expected_data = { + 'data': { + 'id': str(user.id), + 'type': user.__tablename__, + 'attributes': {}, + 'relationships': {} + }, + 'meta': { + 'sqlalchemy_jsonapi_version': '4.0.9' + }, + 'jsonapi': { + 'version': '1.0' + } + } + self.assertEqual(expected_data, serialized_data) + + def test_serialize_single_resource_with_dasherize_true(self): + """Serialize a resource where attributes are dasherized. + + Attribute keys contain dashes instead of underscores. + """ + + class UserSerializer(serializer.JSONAPISerializer): + """Declarative serializer for User.""" + fields = [ + 'id', 'first_name', 'username', + 'age', 'date_joined', 'is_admin'] + model = self.User + dasherize = True + + user = self.User( + first_name='Sally', age=27, is_admin=True, + username='SallySmith1', date_joined=datetime.date(2017, 12, 5)) + self.session.add(user) + self.session.commit() + user = self.session.query(self.User).get(user.id) + + user_serializer = UserSerializer() + serialized_data = user_serializer.serialize(user) + + expected_data = { + 'data': { + 'id': str(user.id), + 'type': u'{}'.format(user.__tablename__), + 'attributes': { + 'date-joined': user.date_joined.isoformat(), + 'username': u'{}'.format(user.username), + 'age': user.age, + 'first-name': u'{}'.format(user.first_name), + 'is-admin': user.is_admin + }, + 'relationships': {} + }, + 'meta': { + 'sqlalchemy_jsonapi_version': '4.0.9' + }, + 'jsonapi': { + 'version': '1.0' + } + } + self.assertEqual(expected_data, serialized_data) + + def test_serialize_single_resource_with_dasherize_false(self): + """Serialize a resource where attributes are not dasherized. + + Attribute keys are underscored like in serializer model. + """ + + class UserSerializer(serializer.JSONAPISerializer): + """Declarative serializer for User.""" + fields = [ + 'id', 'first_name', 'username', + 'age', 'date_joined', 'is_admin'] + model = self.User + dasherize = False + + user = self.User( + first_name='Sally', age=27, is_admin=True, + username='SallySmith1', date_joined=datetime.date(2017, 12, 5)) + self.session.add(user) + self.session.commit() + user = self.session.query(self.User).get(user.id) + + user_serializer = UserSerializer() + serialized_data = user_serializer.serialize(user) + + expected_data = { + 'data': { + 'id': str(user.id), + 'type': u'{}'.format(user.__tablename__), + 'attributes': { + 'date_joined': user.date_joined.isoformat(), + 'username': u'{}'.format(user.username), + 'age': user.age, + 'first_name': u'{}'.format(user.first_name), + 'is_admin': user.is_admin + }, + 'relationships': {} + }, + 'meta': { + 'sqlalchemy_jsonapi_version': '4.0.9' + }, + 'jsonapi': { + 'version': '1.0' + } + } + self.assertEqual(expected_data, serialized_data) + + def test_serialize_collection_of_resources(self): + """Serialize a collection of resources returns a list of objects.""" + + class UserSerializer(serializer.JSONAPISerializer): + """Declarative serializer for User.""" + fields = ['id'] + model = self.User + dasherize = True + + user = self.User( + first_name='Sally', age=27, is_admin=True, + username='SallySmith1', date_joined=datetime.date(2017, 12, 5)) + self.session.add(user) + self.session.commit() + users = self.session.query(self.User) + + user_serializer = UserSerializer() + serialized_data = user_serializer.serialize(users) + + expected_data = { + 'data': [{ + 'id': str(user.id), + 'type': 'users', + 'attributes': {}, + 'relationships': {} + }], + 'meta': { + 'sqlalchemy_jsonapi_version': '4.0.9' + }, + 'jsonapi': { + 'version': '1.0' + } + } + self.assertEquals(expected_data, serialized_data) + + def test_serialize_empty_collection(self): + """Serialize a collection that is empty returns an empty list.""" + + class UserSerializer(serializer.JSONAPISerializer): + """Declarative serializer for User.""" + fields = ['id'] + model = self.User + dasherize = True + + users = self.session.query(self.User) + + user_serializer = UserSerializer() + serialized_data = user_serializer.serialize(users) + + expected_data = { + 'data': [], + 'meta': { + 'sqlalchemy_jsonapi_version': '4.0.9' + }, + 'jsonapi': { + 'version': '1.0' + } + } + self.assertEquals(expected_data, serialized_data) + + def test_serialize_resource_not_found(self): + """Serialize a resource that does not exist returns None.""" + + class UserSerializer(serializer.JSONAPISerializer): + """Declarative serializer for User.""" + fields = ['id'] + model = self.User + dasherize = True + + # Nonexistant user + user = self.session.query(self.User).get(99999999) + + user_serializer = UserSerializer() + serialized_data = user_serializer.serialize(user) + + expected_data = { + 'data': None, + 'meta': { + 'sqlalchemy_jsonapi_version': '4.0.9' + }, + 'jsonapi': { + 'version': '1.0' + } + } + self.assertEqual(expected_data, serialized_data) + + +class SerializeResourceWithRelatedModels(unittest.TestCase): + """Tests for serializing a resource that has related models.""" + + def setUp(self): + """Configure sqlalchemy and session.""" + self.engine = create_engine('sqlite://') + Session = sessionmaker(bind=self.engine) + self.session = Session() + self.Base = declarative_base() + + class User(self.Base): + __tablename__ = 'users' + id = Column(Integer, primary_key=True) + first_name = Column(String(50), nullable=False) + + class Post(self.Base): + __tablename__ = 'posts' + id = Column(Integer, primary_key=True) + title = Column(String(100), nullable=False) + author_id = Column(Integer, ForeignKey('users.id', + ondelete='CASCADE')) + + blog_author = relationship('User', + lazy='joined', + backref=backref('posts', + lazy='dynamic', + cascade='all,delete')) + + self.User = User + self.Post = Post + self.Base.metadata.create_all(self.engine) + + def tearDown(self): + """Reset the sqlalchemy engine.""" + self.Base.metadata.drop_all(self.engine) + + def test_serialize_resource_with_to_many_relationship_success(self): + """Serailize a resource with a to-many relationship.""" + + class UserSerializer(serializer.JSONAPISerializer): + """Declarative serializer for User.""" + fields = ['id', 'first_name'] + model = self.User + dasherize = True + + user = self.User(first_name='Sally') + self.session.add(user) + self.session.commit() + user = self.session.query(self.User).get(user.id) + + user_serializer = UserSerializer() + serialized_data = user_serializer.serialize(user) + + expected_data = { + 'data': { + 'id': str(user.id), + 'type': user.__tablename__, + 'attributes': { + 'first-name': u'{}'.format(user.first_name) + }, + 'relationships': { + 'posts': { + 'links': { + 'self': '/users/1/relationships/posts', + 'related': '/users/1/posts' + } + } + } + }, + 'meta': { + 'sqlalchemy_jsonapi_version': '4.0.9' + }, + 'jsonapi': { + 'version': '1.0' + } + } + self.assertEqual(expected_data, serialized_data) + + def test_serialize_resource_with_to_one_relationship_success(self): + """Serialize a resource with a to-one relationship.""" + + class PostSerializer(serializer.JSONAPISerializer): + """Declarative serializer for Post.""" + fields = ['id', 'title'] + model = self.Post + dasherize = True + + blog_post = self.Post(title='Foo') + self.session.add(blog_post) + self.session.commit() + post = self.session.query(self.Post).get(blog_post.id) + + blog_post_serializer = PostSerializer() + serialized_data = blog_post_serializer.serialize(post) + + expected_data = { + 'data': { + 'id': str(blog_post.id), + 'type': blog_post.__tablename__, + 'attributes': { + 'title': u'{}'.format(blog_post.title) + }, + 'relationships': { + 'blog-author': { + 'links': { + 'self': '/posts/1/relationships/blog-author', + 'related': '/posts/1/blog-author' + } + } + } + }, + 'meta': { + 'sqlalchemy_jsonapi_version': '4.0.9' + }, + 'jsonapi': { + 'version': '1.0' + } + } + self.assertEqual(expected_data, serialized_data) + + def test_serialize_resource_with_relationship_given_dasherize_false(self): + """Serialize a resource with to-one relationship given dasherize false. + + Relationship keys are underscored like in model. + """ + + class PostSerializer(serializer.JSONAPISerializer): + """Declarative serializer for Post.""" + fields = ['id', 'title'] + model = self.Post + dasherize = False + + blog_post = self.Post(title='Foo') + self.session.add(blog_post) + self.session.commit() + post = self.session.query(self.Post).get(blog_post.id) + + blog_post_serializer = PostSerializer() + serialized_data = blog_post_serializer.serialize(post) + + expected_data = { + 'data': { + 'id': str(blog_post.id), + 'type': blog_post.__tablename__, + 'attributes': { + 'title': u'{}'.format(blog_post.title) + }, + 'relationships': { + 'blog_author': { + 'links': { + 'self': '/posts/1/relationships/blog_author', + 'related': '/posts/1/blog_author' + } + } + } + }, + 'meta': { + 'sqlalchemy_jsonapi_version': '4.0.9' + }, + 'jsonapi': { + 'version': '1.0' + } + } + self.assertEqual(expected_data, serialized_data) + + +class TestSerializeErrors(unittest.TestCase): + """Tests for errors raised in serialize method.""" + + def setUp(self): + """Configure sqlalchemy and session.""" + self.engine = create_engine('sqlite://') + Session = sessionmaker(bind=self.engine) + self.session = Session() + self.Base = declarative_base() + + class User(self.Base): + __tablename__ = 'users' + id = Column(Integer, primary_key=True) + first_name = Column(String(50), nullable=False) + + class Post(self.Base): + __tablename__ = 'posts' + id = Column(Integer, primary_key=True) + title = Column(String(100), nullable=False) + author_id = Column(Integer, ForeignKey('users.id', + ondelete='CASCADE')) + + blog_author = relationship('User', + lazy='joined', + backref=backref('posts', + lazy='dynamic', + cascade='all,delete')) + + self.User = User + self.Post = Post + self.Base.metadata.create_all(self.engine) + + def tearDown(self): + """Reset the sqlalchemy engine.""" + self.Base.metadata.drop_all(self.engine) + + def test_serialize_resource_with_mismatched_model(self): + """A serializers model type much match the resource it serializes.""" + + class UserSerializer(serializer.JSONAPISerializer): + """Declarative serializer for User.""" + fields = ['id'] + model = self.Post + dasherize = True + + user = self.User(first_name='Sally') + self.session.add(user) + self.session.commit() + user = self.session.query(self.User).get(user.id) + + user_serializer = UserSerializer() + with self.assertRaises(TypeError): + user_serializer.serialize(user) + + def test_serialize_resource_with_missing_id_field(self): + """An 'id' is required in serializer fields.""" + + class UserSerializer(serializer.JSONAPISerializer): + """Declarative serializer for User.""" + fields = ['first_name'] + model = self.User + dasherize = True + + user = self.User(first_name='Sally') + self.session.add(user) + self.session.commit() + user = self.session.query(self.User).get(user.id) + + user_serializer = UserSerializer() + with self.assertRaises(ValueError): + user_serializer.serialize(user) + + def test_serialize_resource_with_unknown_attribute_in_fields(self): + """Cannot serialize attributes that are unknown to resource.""" + + class UserSerializer(serializer.JSONAPISerializer): + """Declarative serializer for User.""" + fields = ['id', 'firsts_names_unknown'] + model = self.User + dasherize = True + + user = self.User(first_name='Sally') + self.session.add(user) + self.session.commit() + user = self.session.query(self.User).get(user.id) + + user_serializer = UserSerializer() + with self.assertRaises(AttributeError): + user_serializer.serialize(user) + + def test_serialize_resource_with_related_model_in_fields(self): + """Model serializer fields cannot contain related models. + + It is against json-api spec to serialize related models as attributes. + """ + + class UserSerializer(serializer.JSONAPISerializer): + """Declarative serializer for User.""" + fields = ['id', 'posts'] + model = self.User + dasherize = True + + user = self.User(first_name='Sally') + self.session.add(user) + self.session.commit() + user = self.session.query(self.User).get(user.id) + + user_serializer = UserSerializer() + with self.assertRaises(AttributeError): + user_serializer.serialize(user) + + def test_serialize_resource_with_foreign_key_in_fields(self): + """Model serializer fields cannot contain foreign keys. + + It is against json-api spec to serialize foreign keys as attributes. + """ + + class PostSerializer(serializer.JSONAPISerializer): + """Declarative serializer for Post.""" + fields = ['id', 'author_id'] + model = self.Post + dasherize = False + + blog_post = self.Post(title='Foo') + self.session.add(blog_post) + self.session.commit() + post = self.session.query(self.Post).get(blog_post.id) + + blog_post_serializer = PostSerializer() + with self.assertRaises(AttributeError): + blog_post_serializer.serialize(post) + + def test_serialize_resource_with_no_defined_dasherize(self): + """Serializer requires dasherize member.""" + + class UserSerializer(serializer.JSONAPISerializer): + """Declarative serializer for User.""" + fields = ['id', 'firsts_names_unknown'] + model = self.User + + user = self.User(first_name='Sally') + self.session.add(user) + self.session.commit() + user = self.session.query(self.User).get(user.id) + + user_serializer = UserSerializer() + with self.assertRaises(AttributeError): + user_serializer.serialize(user) + + def test_serialize_resource_with_no_defined_model(self): + """Serializer requires model member.""" + + class UserSerializer(serializer.JSONAPISerializer): + """Declarative serializer for User.""" + fields = ['id', 'firsts_names_unknown'] + dasherize = True + + user = self.User(first_name='Sally') + self.session.add(user) + self.session.commit() + user = self.session.query(self.User).get(user.id) + + user_serializer = UserSerializer() + with self.assertRaises(AttributeError): + user_serializer.serialize(user) + + def test_serialize_resource_with_no_defined_fields(self): + """Serializer requires field member.""" + + class UserSerializer(serializer.JSONAPISerializer): + """Declarative serializer for User.""" + dasherize = True + model = self.User + + user = self.User(first_name='Sally') + self.session.add(user) + self.session.commit() + user = self.session.query(self.User).get(user.id) + + user_serializer = UserSerializer() + with self.assertRaises(AttributeError): + user_serializer.serialize(user) diff --git a/sqlalchemy_jsonapi/unittests/test_declarative_serializer.py b/sqlalchemy_jsonapi/unittests/test_declarative_serializer.py deleted file mode 100644 index 1e79110..0000000 --- a/sqlalchemy_jsonapi/unittests/test_declarative_serializer.py +++ /dev/null @@ -1,214 +0,0 @@ -"""Tests for declarative JSONAPISerializer serialize method.""" - -from sqlalchemy_jsonapi.unittests.utils import testcases -from sqlalchemy_jsonapi.unittests import models -from sqlalchemy_jsonapi.declarative import serializer - - -class UserSerializer(serializer.JSONAPISerializer): - """A serializer for the User model.""" - fields = ['id', 'first', 'last', 'username'] - model = models.User - - -class TestDeclarativeSerializer(testcases.SqlalchemyJsonapiTestCase): - """Tests for JSONAPISerializer.serialize.""" - - def test_serialize_a_single_resource_success(self): - """Serialize a single resource successfully. - - A jsonapi compliant object is returned.""" - user = models.User( - first='Sally', last='Smith', - password='password', username='SallySmith1') - self.session.add(user) - self.session.commit() - user = self.session.query(models.User).get(user.id) - - user_serializer = UserSerializer() - serialized_data = user_serializer.serialize(user) - - expected = { - 'data': { - 'type': 'users', - 'id': '1', - 'attributes': { - 'first': u'Sally', - 'last': u'Smith', - 'username': u'SallySmith1' - }, - 'relationships': { - 'posts': { - 'links': { - 'related': '/users/1/posts', - 'self': '/users/1/relationships/posts' - } - }, - 'logs': { - 'links': { - 'related': '/users/1/logs', - 'self': '/users/1/relationships/logs' - } - }, - 'comments': { - 'links': { - 'related': '/users/1/comments', - 'self': '/users/1/relationships/comments' - } - } - } - }, - 'included': [], - 'meta': { - 'sqlalchemy_jsonapi_version': '4.0.9' - }, - 'jsonapi': { - 'version': '1.0' - } - } - self.assertEqual(expected, serialized_data) - - def test_serialize_collection_of_resources(self): - """Serialize a collection of resources successfully returns a jsonapi compliant object.""" - for x in range(2): - user = models.User( - first='Sally-{0}'.format(x+1), - last='Joe-{0}'.format(x+1), - username='SallyJoe{0}'.format(x+1), - password='password') - self.session.add(user) - self.session.commit() - collection = self.session.query(models.User) - - user_serializer = UserSerializer() - actual = user_serializer.serialize(collection) - - expected = { - 'data': [{ - 'type': 'users', - 'id': '1', - 'attributes': { - 'first': u'Sally-1', - 'last': u'Joe-1', - 'username': u'SallyJoe1' - }, - 'relationships': { - 'posts': { - 'links': { - 'related': '/users/1/posts', - 'self': '/users/1/relationships/posts' - } - }, - 'logs': { - 'links': { - 'related': '/users/1/logs', - 'self': '/users/1/relationships/logs' - } - }, - 'comments': { - 'links': { - 'related': '/users/1/comments', - 'self': '/users/1/relationships/comments' - } - } - } - }, { - 'type': 'users', - 'id': '2', - 'attributes': { - 'first': u'Sally-2', - 'last': u'Joe-2', - 'username': u'SallyJoe2' - }, - 'relationships': { - 'posts': { - 'links': { - 'related': '/users/2/posts', - 'self': '/users/2/relationships/posts' - } - }, - 'logs': { - 'links': { - 'related': '/users/2/logs', - 'self': '/users/2/relationships/logs' - } - }, - 'comments': { - 'links': { - 'related': '/users/2/comments', - 'self': '/users/2/relationships/comments' - } - } - } - }], - 'included': [], - 'meta': { - 'sqlalchemy_jsonapi_version': '4.0.9' - }, - 'jsonapi': { - 'version': '1.0' - } - } - self.assertEqual(expected['data'], actual['data']) - self.assertEqual(expected['meta'], actual['meta']) - self.assertEqual(expected['jsonapi'], actual['jsonapi']) - self.assertEqual(expected['included'], actual['included']) - - def test_serialize_resource_with_missing_required_attribute_id(self): - """Serialize resource without required attriute of 'id' raises AttributeError.""" - user = models.User( - first='Sally', last='Smith', - password='password', username='SallySmith1') - self.session.add(user) - self.session.commit() - user = self.session.query(models.User).get(user.id) - - class InvalidUserSerializer(serializer.JSONAPISerializer): - fields = ['first', 'last', 'username'] - model = models.User - - user_serializer = InvalidUserSerializer() - with self.assertRaises(AttributeError): - user_serializer.serialize(user) - - def test_serialize_resource_with_relationships_as_attributes(self): - """Serialize a resource with fields that are relationships raises an AttributeError. - - The serializer should only serialize attributes and not relationships. - Relationships are shown under the resource objects top member 'relationships'. - """ - user = models.User( - first='Sally', last='Smith', - password='password', username='SallySmith1') - self.session.add(user) - blog_post = models.Post( - title='This Is A Title', content='This is the content', - author_id=user.id, author=user) - self.session.add(blog_post) - self.session.commit() - post = self.session.query(models.Post).get(blog_post.id) - - class InvalidBlogPostSerializer(serializer.JSONAPISerializer): - fields = ['title', 'content', 'author', 'comments'] - model = models.Post - - blog_post_serializer = InvalidBlogPostSerializer() - with self.assertRaises(AttributeError): - blog_post_serializer.serialize(post) - - def test_serialize_resource_with_mismatched_resource(self): - """Serialize a resource with mismatched serializer model raises TypeError.""" - user = models.User( - first='Sally', last='Smith', - password='password', username='SallySmith1') - self.session.add(user) - blog_post = models.Post( - title='This Is A Title', content='This is the content', - author_id=user.id, author=user) - self.session.add(blog_post) - self.session.commit() - post = self.session.query(models.Post).get(blog_post.id) - - user_serializer = UserSerializer() - with self.assertRaises(TypeError): - user_serializer.serialize(post) From 045b39c260e008002ddc92ee87e79939cae7c7da Mon Sep 17 00:00:00 2001 From: Kaitlyn Johnson Date: Wed, 5 Apr 2017 11:46:34 -0700 Subject: [PATCH 5/9] Add functionality of having model names dasherized upon serialization of model. --- sqlalchemy_jsonapi/declarative/serializer.py | 86 ++++++++++++++------ 1 file changed, 61 insertions(+), 25 deletions(-) diff --git a/sqlalchemy_jsonapi/declarative/serializer.py b/sqlalchemy_jsonapi/declarative/serializer.py index a5f363e..c855457 100644 --- a/sqlalchemy_jsonapi/declarative/serializer.py +++ b/sqlalchemy_jsonapi/declarative/serializer.py @@ -1,44 +1,53 @@ """A serializer for serializing SQLAlchemy models to JSON API spec.""" +import datetime + +from inflection import dasherize, underscore + class JSONAPISerializer(object): """A JSON API serializer that serializes SQLAlchemy models.""" def serialize(self, resources): - """Serialize resource(s) according to JSON API spec. + """Serialize resource(s) according to json-api spec.""" + try: + self.fields + self.model + self.dasherize + except AttributeError: + raise - Args: - resources: SQLAlchemy model or SQLAlchemy query - """ if 'id' not in self.fields: - raise AttributeError - - serialized_object = { - 'included': [], - 'jsonapi': { - 'version': '1.0' - }, + raise ValueError + serialized = { 'meta': { 'sqlalchemy_jsonapi_version': '4.0.9' + }, + 'jsonapi': { + 'version': '1.0' } } - if isinstance(resources, self.model): - serialized_object['data'] = self._render_resource(resources) - else: - serialized_object['data'] = [] + # Determine multiple resources by checking for SQLAlchemy query count. + if hasattr(resources, 'count'): + serialized['data'] = [] for resource in resources: - serialized_object['data'].append( + serialized['data'].append( self._render_resource(resource)) + else: + serialized['data'] = self._render_resource(resources) - return serialized_object + return serialized def _render_resource(self, resource): - """Renders a resource's top level members based on JSON API spec. + """Renders a resource's top level members based on json-api spec. Top level members include: 'id', 'type', 'attributes', 'relationships' """ - # Must not render a resource that has same named attributes as different model. + if not resource: + return None + # Must not render a resource that has same named + # attributes as different model. if not isinstance(resource, self.model): raise TypeError @@ -54,12 +63,31 @@ def _render_resource(self, resource): def _render_attributes(self, resource): """Render the resources's attributes.""" attributes = {} - related_models = resource.__mapper__.relationships.keys() + attrs_to_ignore = set() + + for key, relationship in resource.__mapper__.relationships.items(): + attrs_to_ignore.update(set( + [column.name for column in relationship.local_columns]).union( + {key})) + + if self.dasherize: + mapped_fields = {x: dasherize(underscore(x)) for x in self.fields} + else: + mapped_fields = {x: x for x in self.fields} + for attribute in self.fields: - if attribute in related_models or attribute == 'id': + if attribute == 'id': continue + # Per json-api spec, we cannot render foreign keys + # or relationsips in attributes. + if attribute in attrs_to_ignore: + raise AttributeError try: - attributes[attribute] = getattr(resource, attribute) + value = getattr(resource, attribute) + if isinstance(value, datetime.datetime): + attributes[mapped_fields[attribute]] = value.isoformat() + else: + attributes[mapped_fields[attribute]] = value except AttributeError: raise @@ -69,13 +97,21 @@ def _render_relationships(self, resource): """Render the resource's relationships.""" relationships = {} related_models = resource.__mapper__.relationships.keys() + if self.dasherize: + mapped_relationships = { + x: dasherize(underscore(x)) for x in related_models} + else: + mapped_relationships = {x: x for x in related_models} + for model in related_models: - relationships[model] = { + relationships[mapped_relationships[model]] = { 'links': { 'self': '/{}/{}/relationships/{}'.format( - resource.__tablename__, resource.id, model), + resource.__tablename__, resource.id, + mapped_relationships[model]), 'related': '/{}/{}/{}'.format( - resource.__tablename__, resource.id, model) + resource.__tablename__, resource.id, + mapped_relationships[model]) } } From b028ca65718910209208d8bdb20d37ecbbd1d454 Mon Sep 17 00:00:00 2001 From: kaitj91 Date: Thu, 6 Apr 2017 10:44:12 -0700 Subject: [PATCH 6/9] Add detailed message about exceptions raised. --- sqlalchemy_jsonapi/declarative/serializer.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/sqlalchemy_jsonapi/declarative/serializer.py b/sqlalchemy_jsonapi/declarative/serializer.py index c855457..1563848 100644 --- a/sqlalchemy_jsonapi/declarative/serializer.py +++ b/sqlalchemy_jsonapi/declarative/serializer.py @@ -18,7 +18,7 @@ def serialize(self, resources): raise if 'id' not in self.fields: - raise ValueError + raise ValueError("Serializer fields must contain 'id'") serialized = { 'meta': { 'sqlalchemy_jsonapi_version': '4.0.9' @@ -49,7 +49,8 @@ def _render_resource(self, resource): # Must not render a resource that has same named # attributes as different model. if not isinstance(resource, self.model): - raise TypeError + raise TypeError( + 'Resource(s) must have be of same type as serializer model.') top_level_members = {} top_level_members['id'] = str(resource.id) From f58c2e324132113e0eb0aab1c8df752b31482fa6 Mon Sep 17 00:00:00 2001 From: kaitj91 Date: Thu, 6 Apr 2017 10:52:28 -0700 Subject: [PATCH 7/9] Reword merror messages and fix typo. --- sqlalchemy_jsonapi/declarative/serializer.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/sqlalchemy_jsonapi/declarative/serializer.py b/sqlalchemy_jsonapi/declarative/serializer.py index 1563848..9adfd1b 100644 --- a/sqlalchemy_jsonapi/declarative/serializer.py +++ b/sqlalchemy_jsonapi/declarative/serializer.py @@ -18,7 +18,7 @@ def serialize(self, resources): raise if 'id' not in self.fields: - raise ValueError("Serializer fields must contain 'id'") + raise ValueError("Serializer fields must contain an 'id'") serialized = { 'meta': { 'sqlalchemy_jsonapi_version': '4.0.9' @@ -48,9 +48,10 @@ def _render_resource(self, resource): return None # Must not render a resource that has same named # attributes as different model. + import pdb; pdb.set_trace() if not isinstance(resource, self.model): raise TypeError( - 'Resource(s) must have be of same type as serializer model.') + 'Resource(s) type must be the same as the serializer model type.') top_level_members = {} top_level_members['id'] = str(resource.id) From 61f6cb0fdf55bc8cfd2e98468b26823739f30d6d Mon Sep 17 00:00:00 2001 From: kaitj91 Date: Thu, 6 Apr 2017 10:56:51 -0700 Subject: [PATCH 8/9] Remove pdb statement. Whoops! --- sqlalchemy_jsonapi/declarative/serializer.py | 1 - 1 file changed, 1 deletion(-) diff --git a/sqlalchemy_jsonapi/declarative/serializer.py b/sqlalchemy_jsonapi/declarative/serializer.py index 9adfd1b..8aef4ae 100644 --- a/sqlalchemy_jsonapi/declarative/serializer.py +++ b/sqlalchemy_jsonapi/declarative/serializer.py @@ -48,7 +48,6 @@ def _render_resource(self, resource): return None # Must not render a resource that has same named # attributes as different model. - import pdb; pdb.set_trace() if not isinstance(resource, self.model): raise TypeError( 'Resource(s) type must be the same as the serializer model type.') From 7afa5d63d8ba542af3ff0b0fb77a743dcb83bc11 Mon Sep 17 00:00:00 2001 From: kaitj91 Date: Mon, 10 Apr 2017 10:05:46 -0700 Subject: [PATCH 9/9] Add default values, including addition of primary_key, and add corresponding tests. --- sqlalchemy_jsonapi/declarative/serializer.py | 37 ++++--- .../declarative_tests/test_serialize.py | 97 +++++++++---------- 2 files changed, 71 insertions(+), 63 deletions(-) diff --git a/sqlalchemy_jsonapi/declarative/serializer.py b/sqlalchemy_jsonapi/declarative/serializer.py index 8aef4ae..a397a33 100644 --- a/sqlalchemy_jsonapi/declarative/serializer.py +++ b/sqlalchemy_jsonapi/declarative/serializer.py @@ -7,18 +7,22 @@ class JSONAPISerializer(object): """A JSON API serializer that serializes SQLAlchemy models.""" + model = None + primary_key = 'id' + fields = [] + dasherize = True + + def __init__(self): + """Ensure required members are not defaults.""" + if self.model is None: + raise TypeError("Model cannot be of type 'None'.") + if self.primary_key not in self.fields: + raise ValueError( + "Serializer fields must contain primary key '{}'".format( + self.primary_key)) def serialize(self, resources): """Serialize resource(s) according to json-api spec.""" - try: - self.fields - self.model - self.dasherize - except AttributeError: - raise - - if 'id' not in self.fields: - raise ValueError("Serializer fields must contain an 'id'") serialized = { 'meta': { 'sqlalchemy_jsonapi_version': '4.0.9' @@ -53,12 +57,14 @@ def _render_resource(self, resource): 'Resource(s) type must be the same as the serializer model type.') top_level_members = {} - top_level_members['id'] = str(resource.id) + try: + top_level_members['id'] = str(getattr(resource, self.primary_key)) + except AttributeError: + raise top_level_members['type'] = resource.__tablename__ top_level_members['attributes'] = self._render_attributes(resource) top_level_members['relationships'] = self._render_relationships( resource) - return top_level_members def _render_attributes(self, resource): @@ -77,7 +83,7 @@ def _render_attributes(self, resource): mapped_fields = {x: x for x in self.fields} for attribute in self.fields: - if attribute == 'id': + if attribute == self.primary_key: continue # Per json-api spec, we cannot render foreign keys # or relationsips in attributes. @@ -98,6 +104,7 @@ def _render_relationships(self, resource): """Render the resource's relationships.""" relationships = {} related_models = resource.__mapper__.relationships.keys() + primary_key_val = getattr(resource, self.primary_key) if self.dasherize: mapped_relationships = { x: dasherize(underscore(x)) for x in related_models} @@ -108,10 +115,12 @@ def _render_relationships(self, resource): relationships[mapped_relationships[model]] = { 'links': { 'self': '/{}/{}/relationships/{}'.format( - resource.__tablename__, resource.id, + resource.__tablename__, + primary_key_val, mapped_relationships[model]), 'related': '/{}/{}/{}'.format( - resource.__tablename__, resource.id, + resource.__tablename__, + primary_key_val, mapped_relationships[model]) } } diff --git a/sqlalchemy_jsonapi/unittests/declarative_tests/test_serialize.py b/sqlalchemy_jsonapi/unittests/declarative_tests/test_serialize.py index 5f6b507..f24d69e 100644 --- a/sqlalchemy_jsonapi/unittests/declarative_tests/test_serialize.py +++ b/sqlalchemy_jsonapi/unittests/declarative_tests/test_serialize.py @@ -298,7 +298,6 @@ class UserSerializer(serializer.JSONAPISerializer): """Declarative serializer for User.""" fields = ['id', 'first_name'] model = self.User - dasherize = True user = self.User(first_name='Sally') self.session.add(user) @@ -340,7 +339,6 @@ class PostSerializer(serializer.JSONAPISerializer): """Declarative serializer for Post.""" fields = ['id', 'title'] model = self.Post - dasherize = True blog_post = self.Post(title='Foo') self.session.add(blog_post) @@ -464,7 +462,6 @@ class UserSerializer(serializer.JSONAPISerializer): """Declarative serializer for User.""" fields = ['id'] model = self.Post - dasherize = True user = self.User(first_name='Sally') self.session.add(user) @@ -475,24 +472,6 @@ class UserSerializer(serializer.JSONAPISerializer): with self.assertRaises(TypeError): user_serializer.serialize(user) - def test_serialize_resource_with_missing_id_field(self): - """An 'id' is required in serializer fields.""" - - class UserSerializer(serializer.JSONAPISerializer): - """Declarative serializer for User.""" - fields = ['first_name'] - model = self.User - dasherize = True - - user = self.User(first_name='Sally') - self.session.add(user) - self.session.commit() - user = self.session.query(self.User).get(user.id) - - user_serializer = UserSerializer() - with self.assertRaises(ValueError): - user_serializer.serialize(user) - def test_serialize_resource_with_unknown_attribute_in_fields(self): """Cannot serialize attributes that are unknown to resource.""" @@ -500,7 +479,6 @@ class UserSerializer(serializer.JSONAPISerializer): """Declarative serializer for User.""" fields = ['id', 'firsts_names_unknown'] model = self.User - dasherize = True user = self.User(first_name='Sally') self.session.add(user) @@ -521,7 +499,6 @@ class UserSerializer(serializer.JSONAPISerializer): """Declarative serializer for User.""" fields = ['id', 'posts'] model = self.User - dasherize = True user = self.User(first_name='Sally') self.session.add(user) @@ -542,7 +519,6 @@ class PostSerializer(serializer.JSONAPISerializer): """Declarative serializer for Post.""" fields = ['id', 'author_id'] model = self.Post - dasherize = False blog_post = self.Post(title='Foo') self.session.add(blog_post) @@ -553,12 +529,16 @@ class PostSerializer(serializer.JSONAPISerializer): with self.assertRaises(AttributeError): blog_post_serializer.serialize(post) - def test_serialize_resource_with_no_defined_dasherize(self): - """Serializer requires dasherize member.""" + def test_serialize_resource_with_invalid_primary_key(self): + """Resource cannot have unknown primary key. + + The primary key must be an attribute on the resource. + """ class UserSerializer(serializer.JSONAPISerializer): - """Declarative serializer for User.""" - fields = ['id', 'firsts_names_unknown'] + """Declarative serializer for Post.""" + fields = ['unknown_primary_key', 'first_name'] + primary_key = 'unknown_primary_key' model = self.User user = self.User(first_name='Sally') @@ -570,36 +550,55 @@ class UserSerializer(serializer.JSONAPISerializer): with self.assertRaises(AttributeError): user_serializer.serialize(user) - def test_serialize_resource_with_no_defined_model(self): + +class TestSerializerInstantiationErrors(unittest.TestCase): + """Test exceptions raised in instantiation of serializer.""" + + def setUp(self): + """Configure sqlalchemy and session.""" + self.engine = create_engine('sqlite://') + Session = sessionmaker(bind=self.engine) + self.session = Session() + self.Base = declarative_base() + + class User(self.Base): + __tablename__ = 'users' + id = Column(Integer, primary_key=True) + first_name = Column(String(50), nullable=False) + + self.User = User + self.Base.metadata.create_all(self.engine) + + def tearDown(self): + """Reset the sqlalchemy engine.""" + self.Base.metadata.drop_all(self.engine) + + def test_serializer_with_no_defined_model(self): """Serializer requires model member.""" class UserSerializer(serializer.JSONAPISerializer): """Declarative serializer for User.""" - fields = ['id', 'firsts_names_unknown'] - dasherize = True + fields = ['id'] - user = self.User(first_name='Sally') - self.session.add(user) - self.session.commit() - user = self.session.query(self.User).get(user.id) + with self.assertRaises(TypeError): + UserSerializer() - user_serializer = UserSerializer() - with self.assertRaises(AttributeError): - user_serializer.serialize(user) + def test_serializer_with_no_defined_fields(self): + """At minimum fields must exist.""" + class UserSerializer(serializer.JSONAPISerializer): + """Declarative serializer for User.""" + model = self.User - def test_serialize_resource_with_no_defined_fields(self): - """Serializer requires field member.""" + with self.assertRaises(ValueError): + UserSerializer() + + def test_serializer_with_missing_id_field(self): + """An 'id' is required in serializer fields.""" class UserSerializer(serializer.JSONAPISerializer): """Declarative serializer for User.""" - dasherize = True + fields = ['first_name'] model = self.User - user = self.User(first_name='Sally') - self.session.add(user) - self.session.commit() - user = self.session.query(self.User).get(user.id) - - user_serializer = UserSerializer() - with self.assertRaises(AttributeError): - user_serializer.serialize(user) + with self.assertRaises(ValueError): + UserSerializer()