UNPKG

9.98 kBJavaScriptView Raw
1var _ = require('lodash');
2var utils = require('./utils');
3
4/**
5 * Constructs a client action factory that uses specific defaults
6 * @type {Function}
7 */
8exports.makeFactoryWithModifier = makeFactoryWithModifier;
9
10/**
11 * Constructs a function that can be called to make a request to ES
12 * @type {Function}
13 */
14exports.factory = makeFactoryWithModifier();
15
16/**
17 * Constructs a proxy to another api method
18 * @type {Function}
19 */
20exports.proxyFactory = exports.factory.proxy;
21
22// export so that we can test this
23exports._resolveUrl = resolveUrl;
24
25exports.ApiNamespace = function() {};
26exports.namespaceFactory = function() {
27 function ClientNamespace(transport, client) {
28 this.transport = transport;
29 this.client = client;
30 }
31
32 ClientNamespace.prototype = new exports.ApiNamespace();
33
34 return ClientNamespace;
35};
36
37function makeFactoryWithModifier(modifier) {
38 modifier = modifier || _.identity;
39
40 var factory = function(spec) {
41 spec = modifier(spec);
42
43 if (!_.isPlainObject(spec.params)) {
44 spec.params = {};
45 }
46
47 if (!spec.method) {
48 spec.method = 'GET';
49 }
50
51 function action(params, cb) {
52 if (typeof params === 'function') {
53 cb = params;
54 params = {};
55 } else {
56 params = params || {};
57 cb = typeof cb === 'function' ? cb : null;
58 }
59
60 try {
61 return exec(this.transport, spec, _.clone(params), cb);
62 } catch (e) {
63 if (typeof cb === 'function') {
64 utils.nextTick(cb, e);
65 } else {
66 var def = this.transport.defer();
67 def.reject(e);
68 return def.promise;
69 }
70 }
71 }
72
73 action.spec = spec;
74
75 return action;
76 };
77
78 factory.proxy = function(fn, spec) {
79 return function(params, cb) {
80 if (typeof params === 'function') {
81 cb = params;
82 params = {};
83 } else {
84 params = params || {};
85 cb = typeof cb === 'function' ? cb : null;
86 }
87
88 if (spec.transform) {
89 spec.transform(params);
90 }
91
92 return fn.call(this, params, cb);
93 };
94 };
95
96 return factory;
97}
98
99var castType = {
100 enum: function validSelection(param, val, name) {
101 if (_.isString(val) && val.indexOf(',') > -1) {
102 val = commaSepList(val);
103 }
104
105 if (_.isArray(val)) {
106 return val
107 .map(function(v) {
108 return validSelection(param, v, name);
109 })
110 .join(',');
111 }
112
113 for (var i = 0; i < param.options.length; i++) {
114 if (param.options[i] === String(val)) {
115 return param.options[i];
116 }
117 }
118 throw new TypeError(
119 'Invalid ' +
120 name +
121 ': expected ' +
122 (param.options.length > 1
123 ? 'one of ' + param.options.join(',')
124 : param.options[0])
125 );
126 },
127 duration: function(param, val, name) {
128 if (utils.isNumeric(val) || utils.isInterval(val)) {
129 return val;
130 } else {
131 throw new TypeError(
132 'Invalid ' +
133 name +
134 ': expected a number or interval ' +
135 '(an integer followed by one of M, w, d, h, m, s, y or ms).'
136 );
137 }
138 },
139 list: function(param, val, name) {
140 switch (typeof val) {
141 case 'number':
142 case 'boolean':
143 return '' + val;
144 case 'string':
145 val = commaSepList(val);
146 /* falls through */
147 case 'object':
148 if (_.isArray(val)) {
149 return val.join(',');
150 }
151 /* falls through */
152 default:
153 throw new TypeError(
154 'Invalid ' +
155 name +
156 ': expected be a comma separated list, array, number or string.'
157 );
158 }
159 },
160 boolean: function(param, val) {
161 val = _.isString(val) ? val.toLowerCase() : val;
162 return val === 'no' || val === 'off' ? false : !!val;
163 },
164 number: function(param, val, name) {
165 if (utils.isNumeric(val)) {
166 return val * 1;
167 } else {
168 throw new TypeError('Invalid ' + name + ': expected a number.');
169 }
170 },
171 string: function(param, val, name) {
172 switch (typeof val) {
173 case 'number':
174 case 'string':
175 return '' + val;
176 default:
177 throw new TypeError('Invalid ' + name + ': expected a string.');
178 }
179 },
180 time: function(param, val, name) {
181 if (typeof val === 'string') {
182 return val;
183 } else if (utils.isNumeric(val)) {
184 return '' + val;
185 } else if (val instanceof Date) {
186 return '' + val.getTime();
187 } else {
188 throw new TypeError('Invalid ' + name + ': expected some sort of time.');
189 }
190 },
191};
192
193function resolveUrl(url, params) {
194 var vars = {};
195 var i;
196 var key;
197
198 if (url.req) {
199 // url has required params
200 if (!url.reqParamKeys) {
201 // create cached key list on demand
202 url.reqParamKeys = _.keys(url.req);
203 }
204
205 for (i = 0; i < url.reqParamKeys.length; i++) {
206 key = url.reqParamKeys[i];
207 if (!params.hasOwnProperty(key) || params[key] == null) {
208 // missing a required param
209 return false;
210 } else {
211 // cast of copy required param
212 if (castType[url.req[key].type]) {
213 vars[key] = castType[url.req[key].type](
214 url.req[key],
215 params[key],
216 key
217 );
218 } else {
219 vars[key] = params[key];
220 }
221 }
222 }
223 }
224
225 if (url.opt) {
226 // url has optional params
227 if (!url.optParamKeys) {
228 url.optParamKeys = _.keys(url.opt);
229 }
230
231 for (i = 0; i < url.optParamKeys.length; i++) {
232 key = url.optParamKeys[i];
233 if (params[key]) {
234 if (castType[url.opt[key].type] || params[key] == null) {
235 vars[key] = castType[url.opt[key].type](
236 url.opt[key],
237 params[key],
238 key
239 );
240 } else {
241 vars[key] = params[key];
242 }
243 } else {
244 vars[key] = url.opt[key]['default'];
245 }
246 }
247 }
248
249 if (!url.template) {
250 // compile the template on demand
251 url.template = _.template(url.fmt);
252 }
253
254 return url.template(
255 _.transform(
256 vars,
257 function(note, val, name) {
258 // encode each value
259 note[name] = encodeURIComponent(val);
260 // remove it from the params so that it isn't sent to the final request
261 delete params[name];
262 },
263 {}
264 )
265 );
266}
267
268function exec(transport, spec, params, cb) {
269 var request = {
270 method: spec.method,
271 };
272 var query = {};
273 var i;
274
275 // pass the timeout from the spec
276 if (spec.requestTimeout) {
277 request.requestTimeout = spec.requestTimeout;
278 }
279
280 if (!params.body && spec.paramAsBody) {
281 if (typeof spec.paramAsBody === 'object') {
282 params.body = {};
283 if (spec.paramAsBody.castToArray) {
284 params.body[spec.paramAsBody.body] = [].concat(
285 params[spec.paramAsBody.param]
286 );
287 } else {
288 params.body[spec.paramAsBody.body] = params[spec.paramAsBody.param];
289 }
290 delete params[spec.paramAsBody.param];
291 } else {
292 params.body = params[spec.paramAsBody];
293 delete params[spec.paramAsBody];
294 }
295 }
296
297 // verify that we have the body if needed
298 if (spec.needsBody && !params.body) {
299 throw new TypeError('A request body is required.');
300 }
301
302 // control params
303 if (spec.bulkBody) {
304 request.bulkBody = true;
305 }
306
307 if (spec.method === 'HEAD') {
308 request.castExists = true;
309 }
310
311 // pick the url
312 if (spec.url) {
313 // only one url option
314 request.path = resolveUrl(spec.url, params);
315 } else {
316 for (i = 0; i < spec.urls.length; i++) {
317 request.path = resolveUrl(spec.urls[i], params);
318 if (request.path) {
319 break;
320 }
321 }
322 }
323
324 if (!request.path) {
325 // there must have been some mimimun requirements that were not met
326 var minUrl = spec.url || spec.urls[spec.urls.length - 1];
327 throw new TypeError(
328 'Unable to build a path with those params. Supply at least ' +
329 _.keys(minUrl.req).join(', ')
330 );
331 }
332
333 // build the query string
334 if (!spec.paramKeys) {
335 // build a key list on demand
336 spec.paramKeys = _.keys(spec.params);
337 spec.requireParamKeys = _.transform(
338 spec.params,
339 function(req, param, key) {
340 if (param.required) {
341 req.push(key);
342 }
343 },
344 []
345 );
346 }
347
348 for (var key in params) {
349 if (params.hasOwnProperty(key) && params[key] != null) {
350 switch (key) {
351 case 'body':
352 case 'headers':
353 case 'requestTimeout':
354 case 'maxRetries':
355 request[key] = params[key];
356 break;
357 case 'ignore':
358 request.ignore = _.isArray(params[key]) ? params[key] : [params[key]];
359 break;
360 case 'method':
361 request.method = utils.toUpperString(params[key]);
362 break;
363 default:
364 var paramSpec = spec.params[key];
365 if (paramSpec) {
366 // param keys don't always match the param name, in those cases it's stored in the param def as "name"
367 paramSpec.name = paramSpec.name || key;
368 if (params[key] != null) {
369 if (castType[paramSpec.type]) {
370 query[paramSpec.name] = castType[paramSpec.type](
371 paramSpec,
372 params[key],
373 key
374 );
375 } else {
376 query[paramSpec.name] = params[key];
377 }
378
379 if (
380 paramSpec['default'] &&
381 query[paramSpec.name] === paramSpec['default']
382 ) {
383 delete query[paramSpec.name];
384 }
385 }
386 } else {
387 query[key] = params[key];
388 }
389 }
390 }
391 }
392
393 for (i = 0; i < spec.requireParamKeys.length; i++) {
394 if (!query.hasOwnProperty(spec.requireParamKeys[i])) {
395 throw new TypeError(
396 'Missing required parameter ' + spec.requireParamKeys[i]
397 );
398 }
399 }
400
401 request.query = query;
402
403 return transport.request(request, cb);
404}
405
406function commaSepList(str) {
407 return str.split(',').map(function(i) {
408 return i.trim();
409 });
410}