Skip to content

Commit

Permalink
Added support for stringifyPrepare/finalize
Browse files Browse the repository at this point in the history
  • Loading branch information
patrick-steele-idem committed Aug 1, 2016
1 parent 60b64d5 commit 9b6597c
Show file tree
Hide file tree
Showing 9 changed files with 230 additions and 184 deletions.
19 changes: 19 additions & 0 deletions README.md
Expand Up @@ -99,6 +99,25 @@ var parse = require('warp10/parse');
var object = parse(json);
```

## JSON stringifyPrepare/finalize

The `stringifyPrepare` function can be used to produce a JavaScript object that is safe to serialize using the native `JSON.stringify` method. The `finalize` method should be called on the parsed object to produce the final object with duplicate objects and circular dependencies intact.

_On the server:_

```javascript
var warp10 = require('warp10').stringifyPrepare;
var object = stringifyPrepare(object); // Returns an Object
var json = JSON.stringify(object);
```

_In the browser:_

```javascript
var finalize = require('warp10/finalize');
var clone = finalize(JSON.parse(json));
```

# Examples

## Serialize examples
Expand Down
1 change: 1 addition & 0 deletions finalize.js
@@ -0,0 +1 @@
module.exports = require('./src/finalize');
50 changes: 50 additions & 0 deletions src/finalize.js
@@ -0,0 +1,50 @@
var isArray = Array.isArray;

function resolve(object, path, len) {
var current = object;
for (var i=0; i<len; i++) {
current = current[path[i]];
}

return current;
}

function resolveType(info) {
if (info.type === 'Date') {
return new Date(info.value);
} else {
throw new Error('Bad type');
}
}

module.exports = function parse(outer) {
var object = outer.object;

var assignments = outer.assignments;
if (assignments) {
for (var i=0, len=assignments.length; i<len; i++) {
var assignment = assignments[i];

var rhs = assignment.r;
var rhsValue;

if (isArray(rhs)) {
rhsValue = resolve(object, rhs, rhs.length);
} else {
rhsValue = resolveType(rhs);
}

var lhs = assignment.l;
var lhsLast = lhs.length-1;

if (lhsLast === -1) {
return rhsValue;
} else {
var lhsParent = resolve(object, lhs, lhsLast);
lhsParent[lhs[lhsLast]] = rhsValue;
}
}
}

return object == null ? null : object;
};
4 changes: 3 additions & 1 deletion src/index.js
@@ -1,4 +1,6 @@
'use strict';
exports.serialize = require('./serialize');
exports.stringify = require('./stringify');
exports.parse = require('./parse');
exports.parse = require('./parse');
exports.finalize = require('./finalize');
exports.stringifyPrepare = require('./stringifyPrepare');
50 changes: 2 additions & 48 deletions src/parse.js
@@ -1,52 +1,6 @@
var isArray = Array.isArray;

function resolve(object, path, len) {
var current = object;
for (var i=0; i<len; i++) {
current = current[path[i]];
}

return current;
}

function resolveType(info) {
if (info.type === 'Date') {
return new Date(info.value);
} else {
throw new Error('Bad type');
}
}
var finalize = require('./finalize');

module.exports = function parse(json) {
var outer = JSON.parse(json);

var object = outer.object;

var assignments = outer.assignments;
if (assignments) {
for (var i=0, len=assignments.length; i<len; i++) {
var assignment = assignments[i];

var rhs = assignment.r;
var rhsValue;

if (isArray(rhs)) {
rhsValue = resolve(object, rhs, rhs.length);
} else {
rhsValue = resolveType(rhs);
}

var lhs = assignment.l;
var lhsLast = lhs.length-1;

if (lhsLast === -1) {
return rhsValue;
} else {
var lhsParent = resolve(object, lhs, lhsLast);
lhsParent[lhs[lhsLast]] = rhsValue;
}
}
}

return object == null ? null : object;
return finalize(outer);
};
144 changes: 9 additions & 135 deletions src/stringify.js
@@ -1,139 +1,6 @@
'use strict';
const markerKey = Symbol('warp10');
const stringifyPrepare = require('./stringifyPrepare');
const escapeEndingScriptTagRegExp = /<\//g;
const isArray = Array.isArray;

class Marker {
constructor(path, symbol) {
this.path = path;
this.symbol = symbol;
}
}

function append(array, el) {
var len = array.length;
var clone = new Array(len+1);
for (var i=0; i<len; i++) {
clone[i] = array[i];
}
clone[len] = el;
return clone;
}

class Assignment {
constructor(lhs, rhs) {
this.l = lhs;
this.r = rhs;
}
}

function handleProperty(clone, key, value, valuePath, serializationSymbol, assignments) {
if (value.constructor === Date) {
assignments.push(new Assignment(valuePath, { type: 'Date', value: value.getTime() }));
} else if (isArray(value)) {
const marker = value[markerKey];

if (marker && marker.symbol === serializationSymbol) {
assignments.push(new Assignment(valuePath, marker.path));
} else {
value[markerKey] = new Marker(valuePath, serializationSymbol);
clone[key] = pruneArray(value, valuePath, serializationSymbol, assignments);
}
} else {
const marker = value[markerKey];
if (marker && marker.symbol === serializationSymbol) {
assignments.push(new Assignment(valuePath, marker.path));
} else {
value[markerKey] = new Marker(valuePath, serializationSymbol);
clone[key] = pruneObject(value, valuePath, serializationSymbol, assignments);
}
}
}

function pruneArray(array, path, serializationSymbol, assignments) {
let len = array.length;

var clone = new Array(len);

for (let i=0; i<len; i++) {
var value = array[i];
if (value == null) {
continue;
}

if (value && typeof value === 'object') {
handleProperty(clone, i, value, append(path, i), serializationSymbol, assignments);
} else {
clone[i] = value;
}
}

return clone;
}

function pruneObject(obj, path, serializationSymbol, assignments) {
var clone = {};

for (var key in obj) {
var value = obj[key];
if (value == null) {
continue;
}

if (value && typeof value === 'object') {
handleProperty(clone, key, value, append(path, key), serializationSymbol, assignments);
} else {
clone[key] = value;
}
}

return clone;
}

function stringifyHelper(obj, safe) {
/**
* Performance notes:
*
* - It is faster to use native JSON.stringify instead of a custom stringify
* - It is faster to first prune and then call JSON.stringify with _no_ replacer
*/
var pruned;

const assignments = []; // Used to keep track of code that needs to run to fix up the stringified object

if (typeof obj === 'object') {
const serializationSymbol = Symbol(); // Used to detect if the marker is associated with _this_ serialization
const path = [];

obj[markerKey] = new Marker(path, serializationSymbol);

if (obj.constructor === Date) {
pruned = null;
assignments.push(new Assignment([], { type: 'Date', value: obj.getTime() }));
} else if (isArray(obj)) {
pruned = pruneArray(obj, path, serializationSymbol, assignments);
} else {
pruned = pruneObject(obj, path, serializationSymbol, assignments);
}
} else {
pruned = obj;
}

var final = {
object: pruned
};

if (assignments.length) {
final.assignments = assignments;
}

let json = JSON.stringify(final);
if (safe) {
json = json.replace(escapeEndingScriptTagRegExp, '\\u003C/');
}

return json;
}

module.exports = function stringify(obj, options) {
if (obj == null) {
Expand All @@ -148,5 +15,12 @@ module.exports = function stringify(obj, options) {
safe = false;
}

return stringifyHelper(obj, safe);
var final = stringifyPrepare(obj);

let json = JSON.stringify(final);
if (safe) {
json = json.replace(escapeEndingScriptTagRegExp, '\\u003C/');
}

return json;
};

0 comments on commit 9b6597c

Please sign in to comment.