UNPKG

5.29 kBJavaScriptView Raw
1/**
2 * Module dependencies
3 */
4
5/**
6 * Expose the Request object
7 */
8
9module.exports = Request;
10
11/**
12 * Create a hyper-path request
13 *
14 * @param {String} path
15 * @param {Client} client
16 */
17
18function Request(path, client, delim) {
19 if (!(this instanceof Request)) return new Request(path, client);
20
21 // init client
22 this.client = client;
23 if (!this.client) throw new Error('hyper-path requires a client to be passed as the second argument');
24
25 this.delim = delim || '.';
26 this.parse(path);
27
28 this._listeners = {};
29 this._scope = {};
30}
31
32/**
33 * Set the root scope
34 *
35 * @param {Object} scope
36 * @return {Request}
37 */
38
39Request.prototype.scope = function(scope) {
40 this._scope = this.wrappedScope ? [scope] : scope;
41 if (this._fn) this.get();
42 return this;
43};
44
45/**
46 * Call a function anytime the data changes in the request
47 *
48 * @param {Function} fn
49 * @return {Request}
50 */
51
52Request.prototype.on = function(fn) {
53 this._fn = fn;
54 this.get();
55 return this;
56};
57
58/**
59 * Refresh the data down the path
60 *
61 * @return {Request}
62 */
63
64Request.prototype.get =
65Request.prototype.refresh = function(fn) {
66 var self = this;
67 var scope = self._scope;
68 fn = fn || self._fn;
69
70 // Clear any previous listeners
71 this.off();
72
73 if (!self.isRoot) return self.traverse(scope, {}, 0, self.path, fn);
74
75 return this._listeners['.'] = self.client.root(function(err, body, links) {
76 if (err) return fn(err);
77 links = links || {};
78 return self.traverse(body || scope, links, 1, self.path, fn);
79 });
80};
81
82/**
83 * Parse the string with the following syntax
84 *
85 * Start at this.scope['path']
86 *
87 * path.to.my.name
88 *
89 * Start at the client's root
90 *
91 * .path.to.my.name
92 *
93 * @param {String} str
94 * @api private
95 */
96
97Request.prototype.parse = function(str) {
98 var path = this.path = Array.isArray(str) ? str.slice() : str.split(this.delim);
99 this.index = path[0];
100 if (path.length === 1) {
101 this.wrappedScope = true;
102 path.unshift(0);
103 }
104 this.isRoot = this.index === '';
105 this.target = path[path.length - 1];
106};
107
108/**
109 * unsubscribe from any emitters for a request
110 *
111 * @return {Request}
112 */
113
114Request.prototype.off = function() {
115 for (var i = 0, listener; i < this._listeners; i++) {
116 listener = this._listener[i];
117 if (listener) listener();
118 }
119 return this;
120};
121
122/**
123 * Traverse properties in the api
124 *
125 * @param {Any} parent
126 * @param {Integer} i
127 * @param {Function} cb
128 * @api private
129 */
130
131Request.prototype.traverse = function(parent, links, i, path, cb) {
132 var request = this;
133
134 // We're done searching
135 if (i >= path.length) return cb(null, normalizeTarget(parent));
136
137 var key = path[i];
138 var value = get(key, parent, links);
139
140 // We couldn't find the property
141 if (!isDefined(value)) {
142 var collection = parent.collection || parent.data;
143 if (collection && collection.hasOwnProperty(key)) return request.traverse(collection, links, i, path, cb);
144 // We have a single hop path so we're going to try going up the prototype.
145 // This is necessary for frameworks like Angular where they use prototypal
146 // inheritance. The risk is getting a value that is on the root Object.
147 // We can at least check that we don't return a function though.
148 if (this.wrappedScope) value = parent[key];
149 if (typeof value === 'function') value = void 0;
150 return cb(null, value);
151 }
152
153 var next = i + 1;
154 var nextProp = path[next];
155
156 // We don't have a link to use or it's set locally on the object
157 if (!value.href || value.hasOwnProperty(nextProp)) return request.traverse(value, links, next, path, cb);
158
159 // We're just getting the link
160 if (nextProp === 'href') return cb(null, value);
161
162 // It's a link
163 var href = value.href;
164
165 var listener = request._listeners[href];
166 var res = request._listeners[href] = request.client.get(href, function(err, body, links) {
167 if (err) return cb(err);
168 if (!body && !links) return cb(null);
169 links = links || {};
170
171 // Be nice to APIs that don't set 'href'
172 if (!body.href) body.href = href;
173
174 var pointer = href.split('#')[1];
175 if (!pointer) return request.traverse(body, links, i + 1, path, cb);
176
177 pointer = pointer.split('/');
178 if (pointer[0] === '') pointer.shift();
179
180 return request.traverse(body, links, 0, pointer, function(err, val) {
181 if (err) return cb(err);
182 return request.traverse(val, links, i + 1, path, cb);
183 });
184 });
185
186 // Unsubscribe and resubscribe if it was previously requested
187 if (listener) listener();
188
189 return res;
190}
191
192/**
193 * Get a value given a key/object
194 *
195 * @api private
196 */
197
198function get(key, parent, fallback) {
199 if (!parent) return undefined;
200 if (parent.hasOwnProperty(key)) return parent[key];
201 if (typeof parent.get === 'function') return parent.get(key);
202 if (fallback.hasOwnProperty(key)) return {href: fallback[key]};
203 return void 0;
204}
205
206/**
207 * If the final object is an collection, pass that back
208 *
209 * @api private
210 */
211
212function normalizeTarget(target) {
213 if (typeof target !== 'object') return target;
214 var href = target.href;
215 target = target.collection || target.data || target; // TODO deprecate 'data'
216 target.href = href;
217 return target;
218}
219
220/**
221 * Check if a value is defined
222 *
223 * @api private
224 */
225
226function isDefined(value) {
227 return typeof value !== 'undefined' && value !== null;
228}