Skip to content

Commit d921d4e

Browse files
authored
Add async deserialization (#51)
closes #46
1 parent 59fe249 commit d921d4e

File tree

3 files changed

+180
-4
lines changed

3 files changed

+180
-4
lines changed

Diff for: README.md

+7
Original file line numberDiff line numberDiff line change
@@ -334,7 +334,14 @@ var data = {
334334
}
335335
};
336336

337+
// Synchronously (blocking)
337338
Serializer.deserialize('article', data);
339+
340+
// Asynchronously (non-blocking)
341+
Serializer.deserializeAsync('article', data)
342+
.then((result) => {
343+
...
344+
});
338345
```
339346

340347
```JSON

Diff for: lib/JSONAPISerializer.js

+48-4
Original file line numberDiff line numberDiff line change
@@ -166,7 +166,7 @@ module.exports = class JSONAPISerializer {
166166
}
167167

168168
/**
169-
* Asynchronously serialze input data to a JSON API compliant response.
169+
* Asynchronously serialize input data to a JSON API compliant response.
170170
* Input data can be a simple object or an array of objects.
171171
*
172172
* @see {@link http://jsonapi.org/format/#document-top-level}
@@ -218,8 +218,8 @@ module.exports = class JSONAPISerializer {
218218
try {
219219
// Serialize a single item of the data-array.
220220
const serializedItem = isDynamicType
221-
? this.serializeMixedData(type, item, included, extraData)
222-
: this.serializeData(type, item, options, included, extraData);
221+
? this.serializeMixedData(type, item, included, extraData)
222+
: this.serializeData(type, item, options, included, extraData);
223223

224224
// If the serialized item is null, we won't push it to the stream,
225225
// as pushing a null-value causes streams to end.
@@ -293,6 +293,49 @@ module.exports = class JSONAPISerializer {
293293
return deserializedData;
294294
}
295295

296+
/**
297+
* Asynchronously Deserialize JSON API document data.
298+
* Input data can be a simple object or an array of objects.
299+
*
300+
* @method JSONAPISerializer#deserializeAsync
301+
* @param {string|Object} type resource's type as string or an object with a dynamic type resolved from data.
302+
* @param {Object} data JSON API input data.
303+
* @param {string} [schema=default] resource's schema name.
304+
* @return {Promise} resolves with serialized data.
305+
*/
306+
deserializeAsync(type, data, schema) {
307+
schema = schema || 'default';
308+
309+
if (!_.isPlainObject(type)) {
310+
if (!this.schemas[type]) {
311+
throw new Error(`No type registered for ${type}`);
312+
}
313+
314+
if (schema && !this.schemas[type][schema]) {
315+
throw new Error(`No schema ${schema} registered for ${type}`);
316+
}
317+
} else {
318+
type = this.validateDynamicTypeOptions(type);
319+
}
320+
321+
return new Promise((resolve, reject) => { // eslint-disable-line consistent-return
322+
if (Array.isArray(data.data)) {
323+
const deserializedData = [];
324+
325+
const stream = intoStream.obj(data.data);
326+
stream.on('data', (item) => {
327+
deserializedData.push(this.deserializeResource(type, item, schema, data.included));
328+
});
329+
330+
stream.on('end', () => resolve(deserializedData));
331+
332+
stream.on('error', reject);
333+
} else {
334+
return resolve(this.deserializeResource(type, data.data, schema, data.included));
335+
}
336+
});
337+
}
338+
296339
/**
297340
* Deserialize a single JSON API resource.
298341
* Input data must be a simple object.
@@ -301,6 +344,7 @@ module.exports = class JSONAPISerializer {
301344
* @param {string|Object} type resource's type as string or an object with a dynamic type resolved from data.
302345
* @param {Object} data JSON API resource data.
303346
* @param {string} [schema=default] resource's schema name.
347+
* @param {Object[]} included.
304348
* @return {Object} deserialized data.
305349
*/
306350
deserializeResource(type, data, schema, included) {
@@ -639,7 +683,7 @@ module.exports = class JSONAPISerializer {
639683
// Support for unpopulated relationships (with mongoDB BSON ObjectId)
640684
serializedRelationship.id = rData.toString();
641685
} else {
642-
// Relationship has been populated
686+
// Relationship has been populated
643687
serializedRelationship.id = rData[rOptions.id].toString();
644688
included.push(this.serializeData(rType, rData, rOptions, included, extraData));
645689
}

Diff for: test/unit/JSONAPISerializer.test.js

+125
Original file line numberDiff line numberDiff line change
@@ -1601,6 +1601,131 @@ describe('JSONAPISerializer', function() {
16011601
});
16021602
});
16031603

1604+
describe('deserializeAsync', function() {
1605+
const Serializer = new JSONAPISerializer();
1606+
Serializer.register('articles', {});
1607+
const dataArray = {
1608+
data: [
1609+
{
1610+
type: 'articles',
1611+
id: '1',
1612+
attributes: {
1613+
title: 'Article 1',
1614+
},
1615+
},
1616+
{ type: 'articles',
1617+
id: '2',
1618+
attributes: {
1619+
title: 'Article 2',
1620+
},
1621+
},
1622+
{ type: 'articles',
1623+
id: '3',
1624+
attributes: {
1625+
title: 'Article 3',
1626+
},
1627+
},
1628+
],
1629+
};
1630+
1631+
it('should return a Promise', () => {
1632+
const promise = Serializer.deserializeAsync('articles', { data: {} });
1633+
expect(promise).to.be.instanceOf(Promise);
1634+
});
1635+
1636+
it('should deserialize simple data', () => {
1637+
const data = {
1638+
data: {
1639+
type: 'article',
1640+
id: '1',
1641+
attributes: {
1642+
title: 'JSON API paints my bikeshed!',
1643+
body: 'The shortest article. Ever.',
1644+
created: '2015-05-22T14:56:29.000Z'
1645+
},
1646+
relationships: {
1647+
author: {
1648+
data: {
1649+
type: 'people',
1650+
id: '1'
1651+
}
1652+
},
1653+
comments: {
1654+
data: [{
1655+
type: 'comment',
1656+
id: '1'
1657+
}, {
1658+
type: 'comment',
1659+
id: '2'
1660+
}]
1661+
}
1662+
}
1663+
}
1664+
};
1665+
1666+
return Serializer.deserializeAsync('articles', data)
1667+
.then((deserializedData) => {
1668+
expect(deserializedData).to.have.property('id');
1669+
expect(deserializedData).to.have.property('title');
1670+
expect(deserializedData).to.have.property('body');
1671+
expect(deserializedData).to.have.property('created');
1672+
expect(deserializedData).to.have.property('author', '1');
1673+
expect(deserializedData).to.have.property('comments').to.be.instanceof(Array).to.eql(['1', '2']);
1674+
});
1675+
});
1676+
1677+
it('should deserialize an array of data', () =>
1678+
Serializer.deserializeAsync('articles', dataArray)
1679+
.then((deserializedData) => {
1680+
expect(deserializedData.length).to.eql(3);
1681+
})
1682+
);
1683+
1684+
it('should deserialize each array item on next tick', () => {
1685+
const tickCounter = new TickCounter(5);
1686+
return Serializer.deserializeAsync('articles', dataArray)
1687+
.then(() => {
1688+
expect(tickCounter.ticks).to.eql(4);
1689+
})
1690+
});
1691+
1692+
it('should throw an error if type has not been registered', function(done) {
1693+
expect(function() {
1694+
Serializer.deserializeAsync('authors', {});
1695+
}).to.throw(Error, 'No type registered for authors');
1696+
done();
1697+
});
1698+
1699+
it('should throw an error if custom schema has not been registered', function(done) {
1700+
expect(function() {
1701+
Serializer.deserializeAsync('articles', {}, 'custom');
1702+
}).to.throw(Error, 'No schema custom registered for articles');
1703+
done();
1704+
});
1705+
1706+
it('should deserialize mixed data with a dynamic type option as the first argument', () => {
1707+
const data = {
1708+
data: {
1709+
type: 'articles',
1710+
id: '1',
1711+
attributes: {
1712+
type: 'articles',
1713+
title: 'JSON API paints my bikeshed!',
1714+
body: 'The shortest article. Ever.'
1715+
}
1716+
}
1717+
};
1718+
1719+
return Serializer.deserializeAsync({type: 'type'}, data)
1720+
.then((deserializedData) => {
1721+
expect(deserializedData).to.have.property('type', 'articles');
1722+
expect(deserializedData).to.have.property('id', '1');
1723+
expect(deserializedData).to.have.property('title');
1724+
expect(deserializedData).to.have.property('body');
1725+
});
1726+
});
1727+
});
1728+
16041729
describe('deserializeMixedData', function() {
16051730
const Serializer = new JSONAPISerializer();
16061731
Serializer.register('article');

0 commit comments

Comments
 (0)