UNPKG

7.01 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, {}, true, 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, body, true, 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 {Object} links
127 * @param {Integer} i
128 * @param {Array} path
129 * @param {Object} parentDocument
130 * @param {Boolean} normalize
131 * @param {Function} cb
132 * @api private
133 */
134
135Request.prototype.traverse = function(parent, links, i, path, parentDocument, normalize, cb) {
136 var self = this;
137
138 // we're done searching
139 if (i >= path.length) return cb(null, normalize ? normalizeTarget(parent) : parent);
140
141 var key = path[i];
142 var value = get(key, parent, links);
143
144 // we couldn't find the property
145 if (!isDefined(value)) return self.handleUndefined(key, parent, links, i, path, parentDocument, normalize, cb);
146
147 var next = i + 1;
148 var nextProp = path[next];
149 var href = value.href;
150
151 // we don't have a link to use or it's set locally on the object
152 if (!href || value.hasOwnProperty(nextProp)) return self.traverse(value, links, next, path, parentDocument, normalize, cb);
153
154 // it's a local pointer
155 if (href.charAt(0) === '#') return self.fetchJsonPath(parentDocument, links, href.slice(1), next, path, normalize, cb);
156
157 // fetch the resource
158 return self.fetchResource(href, next, path, normalize, cb);
159}
160
161/**
162 * Handle an undefined value
163 *
164 * @param {String} key
165 * @param {Object|Array} parent
166 * @param {Object} links
167 * @param {Integer} i
168 * @param {Array} path
169 * @param {Object} parentDocument
170 * @param {Boolean} normalize
171 * @param {Function} cb
172 */
173
174Request.prototype.handleUndefined = function(key, parent, links, i, path, parentDocument, normalize, cb) {
175 // check to make sure it's not on a "normalized" target
176 var collection = normalizeTarget(parent);
177 if (collection && collection.hasOwnProperty(key)) return this.traverse(collection, links, i, path, parentDocument, normalize, cb);
178
179 // We have a single hop path so we're going to try going up the prototype.
180 // This is necessary for frameworks like Angular where they use prototypal
181 // inheritance. The risk is getting a value that is on the root Object.
182 // We can at least check that we don't return a function though.
183 var value;
184 if (this.wrappedScope) value = parent[key];
185 if (typeof value === 'function') value = void 0;
186 return cb(null, value);
187};
188
189/**
190 * Fetch a resource through the client
191 *
192 * @param {String} href
193 * @param {Integer} i
194 * @param {Array} path
195 * @param {Boolean} normalize
196 * @param {Function} cb
197 */
198
199Request.prototype.fetchResource = function(href, i, path, normalize, cb) {
200 var self = this;
201 var orig = href;
202 var parts = orig.split('#');
203 href = parts[0];
204
205 var listener = self._listeners[orig];
206 var res = self._listeners[orig] = self.client.get(href, function(err, body, links) {
207 if (err) return cb(err);
208 if (!body && !links) return cb(null);
209 links = links || {};
210
211 // Be nice to APIs that don't set 'href'
212 if (!body.href) body.href = href;
213
214 if (parts.length === 1) return self.traverse(body, links, i, path, body, normalize, cb);
215 return self.fetchJsonPath(body, links, parts[1], i, path, normalize, cb);
216 });
217
218 // Unsubscribe and resubscribe if it was previously requested
219 if (listener) listener();
220
221 return res;
222};
223
224/**
225 * Traverse a JSON path
226 *
227 * @param {Object} parentDocument
228 * @param {Object} links
229 * @param {String} href
230 * @param {Integer} i
231 * @param {Array} path
232 * @param {Boolean} normalize
233 * @param {Function} cb
234 */
235
236Request.prototype.fetchJsonPath = function(parentDocument, links, href, i, path, normalize, cb) {
237 var self = this;
238 var pointer = href.split('/');
239
240 if (pointer[0] === '') pointer.shift();
241
242 return self.traverse(parentDocument, links, 0, pointer, parentDocument, false, function(err, val) {
243 if (err) return cb(err);
244 if (typeof val === 'object' && !val.href) val.href = parentDocument.href + '#' + href;
245 return self.traverse(val, links, i, path, parentDocument, normalize, cb);
246 });
247};
248
249/**
250 * Get a value given a key/object
251 *
252 * @api private
253 */
254
255function get(key, parent, fallback) {
256 if (!parent) return undefined;
257 if (parent.hasOwnProperty(key)) return parent[key];
258 if (typeof parent.get === 'function') return parent.get(key);
259 if (fallback && fallback.hasOwnProperty(key)) return {href: fallback[key]};
260 return void 0;
261}
262
263/**
264 * If the final object is an collection, pass that back
265 *
266 * @api private
267 */
268
269function normalizeTarget(target) {
270 if (typeof target !== 'object') return target;
271 var href = target.href;
272 target = target.collection || target.data || target; // TODO deprecate 'data'
273 target.href = href;
274 return target;
275}
276
277/**
278 * Check if a value is defined
279 *
280 * @api private
281 */
282
283function isDefined(value) {
284 return typeof value !== 'undefined' && value !== null;
285}