Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 3 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -65,12 +65,14 @@ Serializer.register(type, options);

To avoid repeating the same options for each type, it's possible to add global options on `JSONAPISerializer` instance:

When using convertCase, a LRU cache is utilized for optimization. The default size of the cache is 5000 per conversion type. The size of the cache can be set by passing in a second parameter to the instance. Passing in 0 will result in a LRU cache of infinite size.

```javascript
var JSONAPISerializer = require("json-api-serializer");
var Serializer = new JSONAPISerializer({
convertCase: "kebab-case",
unconvertCase: "camelCase"
});
}, 0);
```

## Usage
Expand Down
47 changes: 34 additions & 13 deletions benchmark/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,10 @@ const JSONAPISerializer = require('..');
const suite = new Benchmark.Suite();

const serializer = new JSONAPISerializer();
const serializerConvert = new JSONAPISerializer({
convertCase: 'kebab-case',
unconvertCase: 'camelCase'
});

const data = [
{
Expand Down Expand Up @@ -51,8 +55,8 @@ const data = [
}
];

serializer.register('article', {
id: 'id', // The attributes to use as the reference. Default = 'id'.
const articleSchema = {
id: 'id',
links: {
// An object or a function that describes links.
self(d) {
Expand All @@ -63,7 +67,7 @@ serializer.register('article', {
relationships: {
// An object defining some relationships.
author: {
type: 'people', // The type of the resource
type: 'people',
links(d) {
// An object or a function that describes Relationships links
return {
Expand Down Expand Up @@ -92,36 +96,47 @@ serializer.register('article', {
},
topLevelLinks: {
// An object or a function that describes top level links.
self: '/articles' // Can be a function (with extra data argument) or a string value
self: '/articles'
}
});
};
serializer.register('article', articleSchema);
serializerConvert.register('article', articleSchema);

// Register 'people' type
serializer.register('people', {
const peopleSchema = {
id: 'id',
links: {
self(d) {
return `/peoples/${d.id}`;
}
}
});
};
serializer.register('people', peopleSchema);
serializerConvert.register('people', peopleSchema);

// Register 'tag' type
serializer.register('tag', {
const tagSchema = {
id: 'id'
});
};
serializer.register('tag', tagSchema);
serializerConvert.register('tag', tagSchema);

// Register 'photo' type
serializer.register('photo', {
const photoSchema = {
id: 'id'
});
};
serializer.register('photo', photoSchema);
serializerConvert.register('photo', photoSchema);

// Register 'comment' type with a custom schema
serializer.register('comment', 'only-body', {
const commentSchema = {
id: '_id'
});
};
serializer.register('comment', 'only-body', commentSchema);
serializerConvert.register('comment', 'only-body', commentSchema);

let serialized;
let serializedConvert;

// Plateform
console.log('Platform info:');
Expand Down Expand Up @@ -160,6 +175,9 @@ suite
.add('serialize', () => {
serialized = serializer.serialize('article', data, { count: 2 });
})
.add('serializeConvertCase', () => {
serializedConvert = serializerConvert.serialize('article', data, { count: 2 });
})
.add('deserializeAsync', {
defer: true,
fn(deferred) {
Expand All @@ -171,6 +189,9 @@ suite
.add('deserialize', () => {
serializer.deserialize('article', serialized);
})
.add('deserializeConvertCase', () => {
serializerConvert.deserialize('article', serializedConvert);
})
.add('serializeError', () => {
const error = new Error('An error occured');
error.status = 500;
Expand Down
31 changes: 26 additions & 5 deletions lib/JSONAPISerializer.js
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,8 @@ const {
set,
toCamelCase,
toKebabCase,
toSnakeCase
toSnakeCase,
LRU
} = require('./helpers');

const { validateOptions, validateDynamicTypeOptions, validateError } = require('./validator');
Expand All @@ -24,11 +25,19 @@ const { validateOptions, validateDynamicTypeOptions, validateError } = require('
*
* @class JSONAPISerializer
* @param {object} [opts] Global options.
* @param {number} [convertCaseCacheSize=5000] Size of cache used for convertCase, 0 results in an infinitely sized cache
*/
module.exports = class JSONAPISerializer {
constructor(opts) {
constructor(opts, convertCaseCacheSize = 5000) {
this.opts = opts || {};
this.schemas = {};

// Cache of strings to convert to their converted values per conversion type
this.convertCaseMap = {
camelCase: new LRU(convertCaseCacheSize),
kebabCase: new LRU(convertCaseCacheSize),
snakeCase: new LRU(convertCaseCacheSize)
};
}

/**
Expand Down Expand Up @@ -794,13 +803,25 @@ module.exports = class JSONAPISerializer {

switch (convertCaseOptions) {
case 'snake_case':
converted = toSnakeCase(data);
converted = this.convertCaseMap.snakeCase.get(data);
if (!converted) {
converted = toSnakeCase(data);
this.convertCaseMap.snakeCase.set(data, converted);
}
break;
case 'kebab-case':
converted = toKebabCase(data);
converted = this.convertCaseMap.kebabCase.get(data);
if (!converted) {
converted = toKebabCase(data);
this.convertCaseMap.kebabCase.set(data, converted);
}
break;
case 'camelCase':
converted = toCamelCase(data);
converted = this.convertCaseMap.camelCase.get(data);
if (!converted) {
converted = toCamelCase(data);
this.convertCaseMap.camelCase.set(data, converted);
}
break;
default: // Do nothing
}
Expand Down
4 changes: 3 additions & 1 deletion lib/helpers.js
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ const {
toCamelCase
} = require('30-seconds-of-code');
const set = require('lodash.set');
const LRU = require('./lru-cache');

// https://github.com/you-dont-need/You-Dont-Need-Lodash-Underscore#_get
const get = (obj, path, defaultValue) =>
Expand All @@ -27,5 +28,6 @@ module.exports = {
transform,
toKebabCase,
toSnakeCase,
toCamelCase
toCamelCase,
LRU
};
109 changes: 109 additions & 0 deletions lib/lru-cache.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,109 @@
// Influenced by http://jsfiddle.net/2baax9nk/5/

class Node {
constructor(key, data) {
this.key = key;
this.data = data;
this.previous = null;
this.next = null;
}
}

module.exports = class LRU {
constructor(capacity) {
this.capacity = capacity === 0 ? Infinity : capacity;
this.map = {};
this.head = null;
this.tail = null;
}

get(key) {
// Existing item
if (this.map[key] !== undefined) {
// Move to the first place
const node = this.map[key];
this._moveFirst(node);

// Return
return node.data;
}

// Not found
return undefined;
}

set(key, value) {
// Existing item
if (this.map[key] !== undefined) {
// Move to the first place
const node = this.map[key];
node.data = value;
this._moveFirst(node);
return;
}

// Ensuring the cache is within capacity
if (Object.keys(this.map).length >= this.capacity) {
const id = this.tail.key;
this._removeLast();
delete this.map[id];
}

// New Item
const node = new Node(key, value);
this._add(node);
this.map[key] = node;
}

_add(node) {
node.next = null;
node.previous = node.next;

// first item
if (this.head === null) {
this.head = node;
this.tail = node;
} else {
// adding to existing items
this.head.previous = node;
node.next = this.head;
this.head = node;
}
}

_remove(node) {
// only item in the cache
if (this.head === node && this.tail === node) {
this.tail = null;
this.head = this.tail;
return;
}

// remove from head
if (this.head === node) {
this.head.next.previous = null;
this.head = this.head.next;
return;
}

// remove from tail
if (this.tail === node) {
this.tail.previous.next = null;
this.tail = this.tail.previous;
return;
}

// remove from middle
node.previous.next = node.next;
node.next.previous = node.previous;
}

_moveFirst(node) {
this._remove(node);
this._add(node);
}

_removeLast() {
this._remove(this.tail);
}
};
2 changes: 1 addition & 1 deletion test/unit/JSONAPISerializer.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -2154,5 +2154,5 @@ describe('JSONAPISerializer', function() {
expect(converted['array-of-number']).to.deep.equal([1, 2, 3, 4, 5]);
expect(converted.date).to.be.a('Date');
});
});
});
});
Loading