diff --git a/mopidy/models.py b/mopidy/models.py index 17616f9d..4861ef0d 100644 --- a/mopidy/models.py +++ b/mopidy/models.py @@ -1,5 +1,7 @@ from __future__ import unicode_literals +import json + class ImmutableObject(object): """ @@ -78,6 +80,7 @@ class ImmutableObject(object): def serialize(self): data = {} + data['__type__'] = self.__class__.__name__ for key in self.__dict__.keys(): public_key = key.lstrip('_') value = self.__dict__[key] @@ -90,6 +93,51 @@ class ImmutableObject(object): return data +class ModelJSONEncoder(json.JSONEncoder): + """ + Automatically serialize Mopidy models to JSON. + + Usage:: + + >>> import json + >>> json.dumps({'a_track': Track(name='name')}, cls=ModelJSONEncoder) + '{"a_track": {"__type__": "Track", "name": "name"}}' + + """ + def default(self, obj): + if isinstance(obj, ImmutableObject): + return obj.serialize() + return json.JSONEncoder.default(self, obj) + + +def model_json_decoder(dct): + """ + Automatically deserialize Mopidy models from JSON. + + Usage:: + + >>> import json + >>> json.loads( + ... '{"a_track": {"__type__": "Track", "name": "name"}}', + ... object_hook=model_json_decoder) + {u'a_track': Track(artists=[], name=u'name')} + + """ + if '__type__' in dct: + obj_type = dct.pop('__type__') + if obj_type == 'Album': + return Album(**dct) + if obj_type == 'Artist': + return Artist(**dct) + if obj_type == 'Playlist': + return Playlist(**dct) + if obj_type == 'TlTrack': + return TlTrack(**dct) + if obj_type == 'Track': + return Track(**dct) + return dct + + class Artist(ImmutableObject): """ :param uri: artist URI diff --git a/tests/models_test.py b/tests/models_test.py index d5d58ace..21ad7ead 100644 --- a/tests/models_test.py +++ b/tests/models_test.py @@ -1,13 +1,16 @@ from __future__ import unicode_literals import datetime +import json -from mopidy.models import Artist, Album, TlTrack, Track, Playlist +from mopidy.models import ( + Artist, Album, TlTrack, Track, Playlist, + ModelJSONEncoder, model_json_decoder) from tests import unittest -class GenericCopyTets(unittest.TestCase): +class GenericCopyTest(unittest.TestCase): def compare(self, orig, other): self.assertEqual(orig, other) self.assertNotEqual(id(orig), id(other)) @@ -83,9 +86,15 @@ class ArtistTest(unittest.TestCase): def test_serialize(self): self.assertDictEqual( - {'uri': 'uri', 'name': 'name'}, + {'__type__': 'Artist', 'uri': 'uri', 'name': 'name'}, Artist(uri='uri', name='name').serialize()) + def test_to_json_and_Back(self): + artist1 = Artist(uri='uri', name='name') + serialized = json.dumps(artist1, cls=ModelJSONEncoder) + artist2 = json.loads(serialized, object_hook=model_json_decoder) + self.assertEqual(artist1, artist2) + def test_eq_name(self): artist1 = Artist(name='name') artist2 = Artist(name='name') @@ -195,15 +204,22 @@ class AlbumTest(unittest.TestCase): def test_serialize_without_artists(self): self.assertDictEqual( - {'uri': 'uri', 'name': 'name'}, + {'__type__': 'Album', 'uri': 'uri', 'name': 'name'}, Album(uri='uri', name='name').serialize()) def test_serialize_with_artists(self): artist = Artist(name='foo') self.assertDictEqual( - {'uri': 'uri', 'name': 'name', 'artists': [artist.serialize()]}, + {'__type__': 'Album', 'uri': 'uri', 'name': 'name', 'artists': + [artist.serialize()]}, Album(uri='uri', name='name', artists=[artist]).serialize()) + def test_to_json_and_back(self): + album1 = Album(uri='uri', name='name', artists=[Artist(name='foo')]) + serialized = json.dumps(album1, cls=ModelJSONEncoder) + album2 = json.loads(serialized, object_hook=model_json_decoder) + self.assertEqual(album1, album2) + def test_eq_name(self): album1 = Album(name='name') album2 = Album(name='name') @@ -386,21 +402,31 @@ class TrackTest(unittest.TestCase): def test_serialize_without_artists(self): self.assertDictEqual( - {'uri': 'uri', 'name': 'name'}, + {'__type__': 'Track', 'uri': 'uri', 'name': 'name'}, Track(uri='uri', name='name').serialize()) def test_serialize_with_artists(self): artist = Artist(name='foo') self.assertDictEqual( - {'uri': 'uri', 'name': 'name', 'artists': [artist.serialize()]}, + {'__type__': 'Track', 'uri': 'uri', 'name': 'name', + 'artists': [artist.serialize()]}, Track(uri='uri', name='name', artists=[artist]).serialize()) def test_serialize_with_album(self): album = Album(name='foo') self.assertDictEqual( - {'uri': 'uri', 'name': 'name', 'album': album.serialize()}, + {'__type__': 'Track', 'uri': 'uri', 'name': 'name', + 'album': album.serialize()}, Track(uri='uri', name='name', album=album).serialize()) + def test_to_json_and_back(self): + track1 = Track( + uri='uri', name='name', album=Album(name='foo'), + artists=[Artist(name='foo')]) + serialized = json.dumps(track1, cls=ModelJSONEncoder) + track2 = json.loads(serialized, object_hook=model_json_decoder) + self.assertEqual(track1, track2) + def test_eq_uri(self): track1 = Track(uri='uri1') track2 = Track(uri='uri1') @@ -590,9 +616,16 @@ class TlTrackTest(unittest.TestCase): repr(TlTrack(tlid=123, track=Track(uri='uri')))) def test_serialize(self): + track = Track(uri='uri', name='name') self.assertDictEqual( - {'tlid': 123, 'track': {'uri': 'uri', 'name': 'name'}}, - TlTrack(tlid=123, track=Track(uri='uri', name='name')).serialize()) + {'__type__': 'TlTrack', 'tlid': 123, 'track': track.serialize()}, + TlTrack(tlid=123, track=track).serialize()) + + def test_to_json_and_back(self): + tl_track1 = TlTrack(tlid=123, track=Track(uri='uri', name='name')) + serialized = json.dumps(tl_track1, cls=ModelJSONEncoder) + tl_track2 = json.loads(serialized, object_hook=model_json_decoder) + self.assertEqual(tl_track1, tl_track2) def test_eq(self): tlid = 123 @@ -719,15 +752,22 @@ class PlaylistTest(unittest.TestCase): def test_serialize_without_tracks(self): self.assertDictEqual( - {'uri': 'uri', 'name': 'name'}, + {'__type__': 'Playlist', 'uri': 'uri', 'name': 'name'}, Playlist(uri='uri', name='name').serialize()) def test_serialize_with_tracks(self): track = Track(name='foo') self.assertDictEqual( - {'uri': 'uri', 'name': 'name', 'tracks': [track.serialize()]}, + {'__type__': 'Playlist', 'uri': 'uri', 'name': 'name', + 'tracks': [track.serialize()]}, Playlist(uri='uri', name='name', tracks=[track]).serialize()) + def test_to_json_and_back(self): + playlist1 = Playlist(uri='uri', name='name') + serialized = json.dumps(playlist1, cls=ModelJSONEncoder) + playlist2 = json.loads(serialized, object_hook=model_json_decoder) + self.assertEqual(playlist1, playlist2) + def test_eq_name(self): playlist1 = Playlist(name='name') playlist2 = Playlist(name='name')