Skip to content

Commit fe4a878

Browse files
alechirschdanivek
authored andcommitted
added performance boost to convert case (#86)
added performance boost to convert case with LRU caching mechanism resolves #84
1 parent 97f71f6 commit fe4a878

File tree

7 files changed

+280
-21
lines changed

7 files changed

+280
-21
lines changed

Diff for: README.md

+3-1
Original file line numberDiff line numberDiff line change
@@ -65,12 +65,14 @@ Serializer.register(type, options);
6565

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

68+
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.
69+
6870
```javascript
6971
var JSONAPISerializer = require("json-api-serializer");
7072
var Serializer = new JSONAPISerializer({
7173
convertCase: "kebab-case",
7274
unconvertCase: "camelCase"
73-
});
75+
}, 0);
7476
```
7577

7678
## Usage

Diff for: benchmark/index.js

+34-13
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,10 @@ const JSONAPISerializer = require('..');
99
const suite = new Benchmark.Suite();
1010

1111
const serializer = new JSONAPISerializer();
12+
const serializerConvert = new JSONAPISerializer({
13+
convertCase: 'kebab-case',
14+
unconvertCase: 'camelCase'
15+
});
1216

1317
const data = [
1418
{
@@ -51,8 +55,8 @@ const data = [
5155
}
5256
];
5357

54-
serializer.register('article', {
55-
id: 'id', // The attributes to use as the reference. Default = 'id'.
58+
const articleSchema = {
59+
id: 'id',
5660
links: {
5761
// An object or a function that describes links.
5862
self(d) {
@@ -63,7 +67,7 @@ serializer.register('article', {
6367
relationships: {
6468
// An object defining some relationships.
6569
author: {
66-
type: 'people', // The type of the resource
70+
type: 'people',
6771
links(d) {
6872
// An object or a function that describes Relationships links
6973
return {
@@ -92,36 +96,47 @@ serializer.register('article', {
9296
},
9397
topLevelLinks: {
9498
// An object or a function that describes top level links.
95-
self: '/articles' // Can be a function (with extra data argument) or a string value
99+
self: '/articles'
96100
}
97-
});
101+
};
102+
serializer.register('article', articleSchema);
103+
serializerConvert.register('article', articleSchema);
98104

99105
// Register 'people' type
100-
serializer.register('people', {
106+
const peopleSchema = {
101107
id: 'id',
102108
links: {
103109
self(d) {
104110
return `/peoples/${d.id}`;
105111
}
106112
}
107-
});
113+
};
114+
serializer.register('people', peopleSchema);
115+
serializerConvert.register('people', peopleSchema);
108116

109117
// Register 'tag' type
110-
serializer.register('tag', {
118+
const tagSchema = {
111119
id: 'id'
112-
});
120+
};
121+
serializer.register('tag', tagSchema);
122+
serializerConvert.register('tag', tagSchema);
113123

114124
// Register 'photo' type
115-
serializer.register('photo', {
125+
const photoSchema = {
116126
id: 'id'
117-
});
127+
};
128+
serializer.register('photo', photoSchema);
129+
serializerConvert.register('photo', photoSchema);
118130

119131
// Register 'comment' type with a custom schema
120-
serializer.register('comment', 'only-body', {
132+
const commentSchema = {
121133
id: '_id'
122-
});
134+
};
135+
serializer.register('comment', 'only-body', commentSchema);
136+
serializerConvert.register('comment', 'only-body', commentSchema);
123137

124138
let serialized;
139+
let serializedConvert;
125140

126141
// Plateform
127142
console.log('Platform info:');
@@ -160,6 +175,9 @@ suite
160175
.add('serialize', () => {
161176
serialized = serializer.serialize('article', data, { count: 2 });
162177
})
178+
.add('serializeConvertCase', () => {
179+
serializedConvert = serializerConvert.serialize('article', data, { count: 2 });
180+
})
163181
.add('deserializeAsync', {
164182
defer: true,
165183
fn(deferred) {
@@ -171,6 +189,9 @@ suite
171189
.add('deserialize', () => {
172190
serializer.deserialize('article', serialized);
173191
})
192+
.add('deserializeConvertCase', () => {
193+
serializerConvert.deserialize('article', serializedConvert);
194+
})
174195
.add('serializeError', () => {
175196
const error = new Error('An error occured');
176197
error.status = 500;

Diff for: lib/JSONAPISerializer.js

+26-5
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,8 @@ const {
99
set,
1010
toCamelCase,
1111
toKebabCase,
12-
toSnakeCase
12+
toSnakeCase,
13+
LRU
1314
} = require('./helpers');
1415

1516
const { validateOptions, validateDynamicTypeOptions, validateError } = require('./validator');
@@ -25,11 +26,19 @@ const { validateOptions, validateDynamicTypeOptions, validateError } = require('
2526
*
2627
* @class JSONAPISerializer
2728
* @param {object} [opts] Global options.
29+
* @param {number} [convertCaseCacheSize=5000] Size of cache used for convertCase, 0 results in an infinitely sized cache
2830
*/
2931
module.exports = class JSONAPISerializer {
30-
constructor(opts) {
32+
constructor(opts, convertCaseCacheSize = 5000) {
3133
this.opts = opts || {};
3234
this.schemas = {};
35+
36+
// Cache of strings to convert to their converted values per conversion type
37+
this.convertCaseMap = {
38+
camelCase: new LRU(convertCaseCacheSize),
39+
kebabCase: new LRU(convertCaseCacheSize),
40+
snakeCase: new LRU(convertCaseCacheSize)
41+
};
3342
}
3443

3544
/**
@@ -790,13 +799,25 @@ module.exports = class JSONAPISerializer {
790799

791800
switch (convertCaseOptions) {
792801
case 'snake_case':
793-
converted = toSnakeCase(data);
802+
converted = this.convertCaseMap.snakeCase.get(data);
803+
if (!converted) {
804+
converted = toSnakeCase(data);
805+
this.convertCaseMap.snakeCase.set(data, converted);
806+
}
794807
break;
795808
case 'kebab-case':
796-
converted = toKebabCase(data);
809+
converted = this.convertCaseMap.kebabCase.get(data);
810+
if (!converted) {
811+
converted = toKebabCase(data);
812+
this.convertCaseMap.kebabCase.set(data, converted);
813+
}
797814
break;
798815
case 'camelCase':
799-
converted = toCamelCase(data);
816+
converted = this.convertCaseMap.camelCase.get(data);
817+
if (!converted) {
818+
converted = toCamelCase(data);
819+
this.convertCaseMap.camelCase.set(data, converted);
820+
}
800821
break;
801822
default: // Do nothing
802823
}

Diff for: lib/helpers.js

+3-1
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ const {
1010
toCamelCase
1111
} = require('30-seconds-of-code');
1212
const set = require('lodash.set');
13+
const LRU = require('./lru-cache');
1314

1415
// https://github.com/you-dont-need/You-Dont-Need-Lodash-Underscore#_get
1516
const get = (obj, path, defaultValue) =>
@@ -29,5 +30,6 @@ module.exports = {
2930
transform,
3031
toKebabCase,
3132
toSnakeCase,
32-
toCamelCase
33+
toCamelCase,
34+
LRU
3335
};

Diff for: lib/lru-cache.js

+109
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,109 @@
1+
// Influenced by http://jsfiddle.net/2baax9nk/5/
2+
3+
class Node {
4+
constructor(key, data) {
5+
this.key = key;
6+
this.data = data;
7+
this.previous = null;
8+
this.next = null;
9+
}
10+
}
11+
12+
module.exports = class LRU {
13+
constructor(capacity) {
14+
this.capacity = capacity === 0 ? Infinity : capacity;
15+
this.map = {};
16+
this.head = null;
17+
this.tail = null;
18+
}
19+
20+
get(key) {
21+
// Existing item
22+
if (this.map[key] !== undefined) {
23+
// Move to the first place
24+
const node = this.map[key];
25+
this._moveFirst(node);
26+
27+
// Return
28+
return node.data;
29+
}
30+
31+
// Not found
32+
return undefined;
33+
}
34+
35+
set(key, value) {
36+
// Existing item
37+
if (this.map[key] !== undefined) {
38+
// Move to the first place
39+
const node = this.map[key];
40+
node.data = value;
41+
this._moveFirst(node);
42+
return;
43+
}
44+
45+
// Ensuring the cache is within capacity
46+
if (Object.keys(this.map).length >= this.capacity) {
47+
const id = this.tail.key;
48+
this._removeLast();
49+
delete this.map[id];
50+
}
51+
52+
// New Item
53+
const node = new Node(key, value);
54+
this._add(node);
55+
this.map[key] = node;
56+
}
57+
58+
_add(node) {
59+
node.next = null;
60+
node.previous = node.next;
61+
62+
// first item
63+
if (this.head === null) {
64+
this.head = node;
65+
this.tail = node;
66+
} else {
67+
// adding to existing items
68+
this.head.previous = node;
69+
node.next = this.head;
70+
this.head = node;
71+
}
72+
}
73+
74+
_remove(node) {
75+
// only item in the cache
76+
if (this.head === node && this.tail === node) {
77+
this.tail = null;
78+
this.head = this.tail;
79+
return;
80+
}
81+
82+
// remove from head
83+
if (this.head === node) {
84+
this.head.next.previous = null;
85+
this.head = this.head.next;
86+
return;
87+
}
88+
89+
// remove from tail
90+
if (this.tail === node) {
91+
this.tail.previous.next = null;
92+
this.tail = this.tail.previous;
93+
return;
94+
}
95+
96+
// remove from middle
97+
node.previous.next = node.next;
98+
node.next.previous = node.previous;
99+
}
100+
101+
_moveFirst(node) {
102+
this._remove(node);
103+
this._add(node);
104+
}
105+
106+
_removeLast() {
107+
this._remove(this.tail);
108+
}
109+
};

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

+1-1
Original file line numberDiff line numberDiff line change
@@ -2228,5 +2228,5 @@ describe('JSONAPISerializer', function() {
22282228
expect(converted['array-of-number']).to.deep.equal([1, 2, 3, 4, 5]);
22292229
expect(converted.date).to.be.a('Date');
22302230
});
2231-
});
2231+
});
22322232
});

0 commit comments

Comments
 (0)