extraction
Advanced tools
Comparing version 0.9.0 to 0.9.1
{ | ||
"name": "extraction", | ||
"version": "0.9.0", | ||
"version": "0.9.1", | ||
"description": "Tree Extraction for JavaScript Object Graphs", | ||
@@ -5,0 +5,0 @@ "main": "lib/extraction.js", |
@@ -45,3 +45,3 @@ /* | ||
},{"./extraction-dsl.js":4,"./extraction-seen.js":7}],6:[function(_dereq_,module,exports){ | ||
"use strict";function _typeof(e){return e&&"undefined"!=typeof Symbol&&e.constructor===Symbol?"symbol":typeof e}Object.defineProperty(exports,"__esModule",{value:!0});var reifyInternal=function e(t,r,n,o){r.procValueBefore&&(t=r.procValueBefore(t,n)),r.debug&&console.log("reify: DEBUG: path: "+n+", graph: "+("undefined"==typeof t?"undefined":_typeof(t)));var f=!1;if(f=r.isReference?r.isReference(t):"string"==typeof t&&t.match(/^@self(?:\..+|)$/)){var i=void 0;if(i=r.getObject?r.getObject(t):o[t],void 0===i)throw new Error('invalid object reference "'+t+'" at path "'+n+'"');t=i}else if(null!==t&&"object"===("undefined"==typeof t?"undefined":_typeof(t)))if(r.setObject?r.setObject(n,t):o[n]=t,t instanceof Array)for(var l=0;l<t.length;l++)t[l]=e(t[l],r,n+"."+l,o);else for(var a in t)Object.hasOwnProperty.call(t,a)&&(t[a]=e(t[a],r,n+"."+a,o));return r.procValueAfter&&(t=r.procValueAfter(t,n)),t},reify=function(e,t){if("object"!==("undefined"==typeof e?"undefined":_typeof(e)))throw new Error("invalid graph argument (expected object type)");if(null===e)throw new Error("invalid graph argument (expected not-null value)");if(void 0===t&&(t={}),"object"!==("undefined"==typeof t?"undefined":_typeof(t)))throw new Error("invalid options argument (expected object type)");return reifyInternal(e,t,"@self",{"@self":e})};exports["default"]=reify; | ||
"use strict";function _typeof(e){return e&&"undefined"!=typeof Symbol&&e.constructor===Symbol?"symbol":typeof e}Object.defineProperty(exports,"__esModule",{value:!0});var reifyInternal=function e(t,r,n,o){r.procValueBefore&&(t=r.procValueBefore(t,n)),r.debug&&console.log("reify: DEBUG: path: "+n+", graph: "+("undefined"==typeof t?"undefined":_typeof(t)));var f=!1;if(f=r.isReference?r.isReference(t):"string"==typeof t&&t.match(/^@self(?:\..+|)$/)){var i=void 0;if(i=r.getObject?r.getObject(t,n):o[t],void 0===i)throw new Error('invalid object reference "'+t+'" at path "'+n+'"');t=i}else if(null!==t&&"object"===("undefined"==typeof t?"undefined":_typeof(t)))if(r.setObject?r.setObject(t,n):o[n]=t,t instanceof Array)for(var l=0;l<t.length;l++)t[l]=e(t[l],r,n+"."+l,o);else for(var a in t)Object.hasOwnProperty.call(t,a)&&(t[a]=e(t[a],r,n+"."+a,o));return r.procValueAfter&&(t=r.procValueAfter(t,n)),t},reify=function(e,t){if("object"!==("undefined"==typeof e?"undefined":_typeof(e)))throw new Error("invalid graph argument (expected object type)");if(null===e)throw new Error("invalid graph argument (expected not-null value)");if(void 0===t&&(t={}),"object"!==("undefined"==typeof t?"undefined":_typeof(t)))throw new Error("invalid options argument (expected object type)");return reifyInternal(e,t,"@self",{"@self":e})};exports["default"]=reify; | ||
},{}],7:[function(_dereq_,module,exports){ | ||
@@ -48,0 +48,0 @@ "use strict";function _classCallCheck(e,n){if(!(e instanceof n))throw new TypeError("Cannot call a class as a function")}var _createClass=function(){function e(e,n){for(var t=0;t<n.length;t++){var s=n[t];s.enumerable=s.enumerable||!1,s.configurable=!0,"value"in s&&(s.writable=!0),Object.defineProperty(e,s.key,s)}}return function(n,t,s){return t&&e(n.prototype,t),s&&e(n,s),n}}();Object.defineProperty(exports,"__esModule",{value:!0});var Seen=function(){return"function"==typeof WeakMap?function(){function e(){_classCallCheck(this,e),this.seenMap=new WeakMap}return _createClass(e,[{key:"set",value:function(e,n){this.seenMap.set(e,n)}},{key:"get",value:function(e){return this.seenMap.get(e)}}]),e}():function(){function e(){_classCallCheck(this,e),this.seenSeq=[],this.seenMap={}}return _createClass(e,[{key:"set",value:function(e,n){this.seenMap[this.seenSeq.length]=n,this.seenSeq.push(e)}},{key:"get",value:function(e){var n=this.seenSeq.indexOf(e);return n>=0?this.seenMap[n]:void 0}}]),e}()}();exports["default"]=Seen; |
{ | ||
"name": "extraction", | ||
"version": "0.9.0", | ||
"version": "0.9.1", | ||
"description": "Tree Extraction for JavaScript Object Graphs", | ||
@@ -5,0 +5,0 @@ "keywords": [ "tree", "graph", "data", "structure", "extraction", "object" ], |
255
README.md
@@ -16,3 +16,11 @@ | ||
Extraction is a very small JavaScript library... FIXME | ||
Extraction is a small JavaScript library for extracting object trees | ||
from arbitrary object graphs. Object graphs usually have cycles and | ||
contain many information. Hence, the clue is that the extracted object | ||
trees use links to break reference cycles and can be just partial by | ||
leaving out information. The Extraction library is intended for two | ||
main use cases: to support the persisting and restoring of arbitrary | ||
in-memory object graph structures (where the cycle problem has to be | ||
resolved) and to support the generation of responses in REST APIs based | ||
on object graphs (where the partial information has to be resolved). | ||
@@ -37,2 +45,9 @@ Installation | ||
The Extraction library exposes two API functions (signatures given in TypeScript notation): | ||
### `extract` | ||
This is the main API method for extracting an object tree from an object | ||
graph with the help of a tree extraction DSL. | ||
``` | ||
@@ -42,2 +57,47 @@ extraction.extract(graph: object, spec: string, options?: object): object | ||
- The `graph` argument has to be an Array of Object and be any start node in the graph. | ||
- The `spec` argument is the tree extraction specification Domain-Specific Language (DSL). | ||
It has to follow the following PEG-style grammar: | ||
RHS | | LHS | ||
---------|-----|--------------------------- | ||
spec | ::= | object / array | ||
object | ::= | `"{"` content? `"}"` | ||
array | ::= | `"["` content? `"]"` | ||
content | ::= | (`"->"` num) / (field (`","` field)*) | ||
field | ::= | (property spec) / (`"!"`? property) | ||
property | ::= | id / `"*"` / (num `".."` num) / num | ||
num | ::= | (`"-"`? `[0-9]`+) / `"-oo"` / `"oo"` | ||
id | ::= | `[$a-zA-Z_][$a-zA-Z0-9_]`\* | ||
Hint: the matching of multiple `field` in `content` follows a last-match semantic! | ||
- The `options` argument is optional and can contain the following properties: | ||
- `procValueBefore: (value: any) => any`:<br/> | ||
Pre-process a value (object or property value) before it is taken into account. | ||
A caller could use this to convert the value from a custom type into a standard | ||
JavaScript type. | ||
- `procValueAfter: (value: any) => any`:<br/> | ||
Post-process a value (object or property value) after it was taken into account. | ||
A caller could use this to convert the value into an external representation | ||
like JSON or XML. | ||
- `makeRefValue: (pathFirst: string, pathNow: string, obj: Object) => any`:<br/> | ||
Make an object reference out of an object `obj`, which is now found (again) | ||
at path `pathNow` and the first-time found at `pathFirst`. The default | ||
is to use `pathFirst` as the reference, but a caller could also use | ||
a stub for `obj` (usually based on just the OID of it) as the reference. | ||
- `debug: boolean`:<br/> | ||
Print debug information about internal processing. | ||
### `reify` | ||
This is a utility API method to re-generate an object graph from an | ||
object tree by reifying all self-references back to the referenced | ||
objects. | ||
``` | ||
@@ -47,2 +107,195 @@ extraction.reify(tree: object, options?: object): object | ||
- The `tree` argument is the root of an object tree which should be traversed. | ||
- The `options` argument is optional and can contain the following properties: | ||
- `procValueBefore: (value: any) => any`:<br/> | ||
Pre-process a value (object or property value) after it is taken into account. | ||
A caller could use this to convert the value from an external representation | ||
like JSON or XML. | ||
- `procValueAfter: (value: any) => any`:<br/> | ||
Post-process a value (object or property value) after it was taken into account. | ||
A caller could use this to convert the value from a standard type into a custom | ||
JavaScript type. | ||
- `isReference: (value: any) => boolean`:<br/> | ||
Determine whether `value` is an object reference. | ||
- `getObject: (value: any, path: string) => any`:<br/> | ||
Fetch the underlying object from an object reference `value`, found at `path`. | ||
- `setObject: (value: any, path: string) => void`:<br/> | ||
Store an underlying object `value`, found at `path`. | ||
- `debug: boolean`:<br/> | ||
Print debug information about internal processing. | ||
Example | ||
------- | ||
Suppose we have an object graph (aka "business model") based | ||
on two entity definitions (in pseudo language): | ||
``` | ||
Person { | ||
id: number | ||
name: string | ||
tags: string+ | ||
home: Location | ||
rival: Person? | ||
} | ||
Location { | ||
id: number | ||
name: string | ||
owner: Person? | ||
subs: Location* | ||
} | ||
``` | ||
A possible JavaScript instanciation of this object graph definition then | ||
could be: | ||
```js | ||
var Graph = { | ||
Person: [ | ||
{ id: 7, name: "God", tags: [ "good", "nice" ] }, | ||
{ id: 666, name: "Devil", tags: [ "bad", "cruel" ] } | ||
], | ||
Location: [ | ||
{ id: 0, name: "World" }, | ||
{ id: 1, name: "Heaven" }, | ||
{ id: 999, name: "Hell" } | ||
] | ||
} | ||
Graph.Person[0].home = Graph.Location[1] | ||
Graph.Person[1].home = Graph.Location[2] | ||
Graph.Person[1].rival = Graph.Person[0] | ||
Graph.Person[0].rival = Graph.Person[1] | ||
Graph.Location[0].subs = [ Graph.Location[1], Graph.Location[2] ] | ||
Graph.Location[1].owner = Graph.Person[0] | ||
Graph.Location[2].owner = Graph.Person[1] | ||
``` | ||
Because of the relationship cycles in this graph, you cannot easily | ||
serialize this graph as JSON with plain `JSON.stringify()` as it | ||
will detect but not handle the cycles correctly. With the Extraction library | ||
you can serialize and deseralize this graph just fine: | ||
```js | ||
/* import external requirements */ | ||
import { extract, reify } from "extraction" | ||
import { expect } from "chai" | ||
import { inspect } from "util" | ||
/* extract entire graph as a tree with self-references */ | ||
let tree = extract(Graph, "{ -> oo }") | ||
console.log(inspect(tree, { depth: null })) | ||
// { Person: | ||
// [ { id: 7, | ||
// name: 'God', | ||
// tags: [ 'good', 'nice' ], | ||
// home: { id: 1, name: 'Heaven', owner: '@self.Person.0' }, | ||
// rival: | ||
// { id: 666, | ||
// name: 'Devil', | ||
// tags: [ 'bad', 'cruel' ], | ||
// home: { id: 999, name: 'Hell', owner: '@self.Person.0.rival' }, | ||
// rival: '@self.Person.0' } }, | ||
// '@self.Person.0.rival' ], | ||
// Location: | ||
// [ { id: 0, | ||
// name: 'World', | ||
// subs: [ '@self.Person.0.home', '@self.Person.0.rival.home' ] }, | ||
// '@self.Person.0.home', | ||
// '@self.Person.0.rival.home' ] } | ||
/* as the tree has no cycles, it can be serialized/unserialized just fine */ | ||
tree = JSON.parse(JSON.stringify(tree)) | ||
/* reify the object references to gain the original graph again */ | ||
let GraphNew = reify(tree) | ||
expect(GraphNew).to.be.deep.equal(Graph) | ||
``` | ||
Now suppose we have a REST API where we want to let Persons | ||
with their home Location be queried: | ||
```js | ||
/* import external requirements */ | ||
import HAPI from "hapi" | ||
import { extract } from "./lib/extraction" | ||
/* import sample graph */ | ||
import Graph from "./sample-graph" | ||
/* establish a new REST service */ | ||
var server = new HAPI.Server() | ||
server.connection({ address: "0.0.0.0", port: "12345" }) | ||
/* provide REST endpoints */ | ||
server.route({ | ||
method: "GET", | ||
path: "/persons/{id}", | ||
handler: (request, reply) => { | ||
let id = parseInt(request.params.id) | ||
let person = Graph.Person.find((person) => person.id === id) | ||
let response = JSON.stringify(extract( | ||
person, "{ id, name, home { id, name } }" | ||
)) | ||
reply(response) | ||
} | ||
}) | ||
/* fire up REST service */ | ||
server.start((err) => { | ||
if (err) | ||
console.log(err) | ||
}) | ||
``` | ||
Querying the two Persons yields: | ||
``` | ||
$ curl http://127.0.0.1:12345/persons/7 | ||
{"id":7,"name":"God","home":{"id":1,"name":"Heaven"}} | ||
$ curl http://127.0.0.1:12345/persons/6660 | ||
{"id":666,"name":"Devil","home":{"id":999,"name":"Hell"}} | ||
``` | ||
Finally, instead of extracting a tree and then encoding it | ||
as JSON, you can immediately encode it during extraction: | ||
```js | ||
extraction.extract(Graph, "{ -> oo }", { | ||
procValueAfter: (value) => { | ||
if (typeof value === "object" && value !== null) { | ||
if (value instanceof Array) | ||
value = "[" + value.join(",") + "]" | ||
else | ||
value = "{" + Object.keys(value).map(function (key) { | ||
return JSON.stringify(key) + ":" + value[key] | ||
}).join(",") + "}" | ||
} | ||
else | ||
value = JSON.stringify(value) | ||
return value | ||
} | ||
})) | ||
// {"Person":[{"id":7,"name":"God","tags":["good","nice"], | ||
// "home":{"id":1,"name":"Heaven","owner":"@self.Person.0"}, | ||
// "rival":{"id":666,"name":"Devil","tags":["bad","cruel"], | ||
// "home":{"id":999,"name":"Hell","owner":"@self.Person.0.rival"}, | ||
// "rival":"@self.Person.0"}},"@self.Person.0.rival"], | ||
// "Location":[{"id":0,"name":"World","subs":["@self.Person.0.home", | ||
// "@self.Person.0.rival.home"]},"@self.Person.0.home", | ||
// "@self.Person.0.rival.home"]} | ||
``` | ||
Implementation Notice | ||
@@ -49,0 +302,0 @@ --------------------- |
@@ -45,3 +45,3 @@ /* | ||
if (options.getObject) | ||
obj = options.getObject(node) | ||
obj = options.getObject(node, path) | ||
else | ||
@@ -56,3 +56,3 @@ obj = map[node] | ||
if (options.setObject) | ||
options.setObject(path, node) | ||
options.setObject(node, path) | ||
else | ||
@@ -59,0 +59,0 @@ map[path] = node |
/* | ||
** Extraction -- Data Structure Extraction for JavaScript | ||
** Extraction -- Tree Extraction for JavaScript Object Graphs | ||
** Copyright (c) 2015 Ralf S. Engelschall <rse@engelschall.com> | ||
@@ -4,0 +4,0 @@ ** |
/* | ||
** Extraction -- Data Structure Extraction for JavaScript | ||
** Extraction -- Tree Extraction for JavaScript Object Graphs | ||
** Copyright (c) 2015 Ralf S. Engelschall <rse@engelschall.com> | ||
@@ -10,3 +10,3 @@ ** | ||
** distribute, sublicense, and/or sell copies of the Software, and to | ||
** permit persons to whom the Software is furnished to do so, subject to | ||
** permit Person to whom the Software is furnished to do so, subject to | ||
** the following conditions: | ||
@@ -33,23 +33,4 @@ ** | ||
var extraction = require("../lib/extraction.js") | ||
var Graph = require("../smp/graph.js") | ||
/* the sample graph */ | ||
var persons = [ | ||
{ id: 7, name: "God", tags: [ "good", "nice" ] }, | ||
{ id: 666, name: "Devil", tags: [ "bad", "cruel" ] } | ||
] | ||
var locations = [ | ||
{ id: 1, name: "Heaven" }, | ||
{ id: 999, name: "Hell" } | ||
] | ||
persons[0].location = locations[0] | ||
persons[1].location = locations[1] | ||
persons[1].counterpart = persons[0] | ||
persons[0].counterpart = persons[1] | ||
locations[0].owner = persons[0] | ||
locations[0].owner = persons[1] | ||
var graph = { | ||
persons: persons, | ||
locations: locations | ||
} | ||
describe("Extraction Library", function () { | ||
@@ -62,38 +43,17 @@ it("should expose its official API", function () { | ||
it("should extract simply", function () { | ||
expect(extraction.extract(graph, "{}", { ignoreMatchErrors: true, debug: true })) | ||
expect(extraction.extract(Graph, "{}", { ignoreMatchErrors: true, debug: false })) | ||
.to.be.deep.equal({}) | ||
expect(extraction.extract(graph, "{ * }", { ignoreMatchErrors: true, debug: true })) | ||
.to.be.deep.equal({ persons: [], locations: [] }) | ||
expect(extraction.extract(graph, "{ persons [ * { id, name } ] }", { ignoreMatchErrors: true, debug: true })) | ||
.to.be.deep.equal({ persons: [ { id: 7, name: "God" }, { id: 666, name: "Devil" } ] }) | ||
extraction.extract(graph, "{ persons [ * { id, name } ] }") | ||
extraction.extract(graph, "{ persons [ * { id, name } ] }") | ||
/* | ||
console.log(require("util").inspect(extraction(graph, | ||
"{ persons [ * { id, tags [ -> oo ] } ], !locations }" | ||
), { depth: null, colors: true })) | ||
console.log(require("util").inspect(extraction(graph, | ||
"{ -> oo }" | ||
), { depth: null, colors: true })) | ||
*/ | ||
console.log(extraction.extract(graph, "{ -> oo }", { | ||
procValueAfter: (value) => { | ||
if (typeof value === "object" && value !== null) { | ||
if (value instanceof Array) | ||
value = "[" + value.join(",") + "]" | ||
else | ||
value = "{" + Object.keys(value).map(function (key) { | ||
return JSON.stringify(key) + ":" + value[key] | ||
}).join(",") + "}" | ||
} | ||
else | ||
value = JSON.stringify(value) | ||
return value | ||
} | ||
})) | ||
var g = extraction.extract(graph, "{ -> oo }") | ||
expect(extraction.extract(Graph, "{ * }", { ignoreMatchErrors: true, debug: false })) | ||
.to.be.deep.equal({ Person: [], Location: [] }) | ||
expect(extraction.extract(Graph, "{ Person [ * { id, name } ] }", { ignoreMatchErrors: true, debug: false })) | ||
.to.be.deep.equal({ Person: [ { id: 7, name: "God" }, { id: 666, name: "Devil" } ] }) | ||
extraction.extract(Graph, "{ Person [ * { id, name } ] }") | ||
extraction.extract(Graph, "{ Person [ * { id, name } ] }") | ||
}) | ||
it("should fully extract and reify again", function () { | ||
var g = extraction.extract(Graph, "{ -> oo }") | ||
extraction.reify(g) | ||
expect(g).to.be.deep.equal(graph) | ||
expect(g).to.be.deep.equal(Graph) | ||
}) | ||
}) | ||
84613
19
909
328