1 | var _ = require('lodash');
|
2 | var utils = require('./utils');
|
3 |
|
4 |
|
5 |
|
6 |
|
7 |
|
8 | exports.makeFactoryWithModifier = makeFactoryWithModifier;
|
9 |
|
10 |
|
11 |
|
12 |
|
13 |
|
14 | exports.factory = makeFactoryWithModifier();
|
15 |
|
16 |
|
17 |
|
18 |
|
19 |
|
20 | exports.proxyFactory = exports.factory.proxy;
|
21 |
|
22 |
|
23 | exports._resolveUrl = resolveUrl;
|
24 |
|
25 | exports.ApiNamespace = function() {};
|
26 | exports.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 |
|
37 | function 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 |
|
99 | var 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 |
|
147 | case 'object':
|
148 | if (_.isArray(val)) {
|
149 | return val.join(',');
|
150 | }
|
151 |
|
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 |
|
193 | function resolveUrl(url, params) {
|
194 | var vars = {};
|
195 | var i;
|
196 | var key;
|
197 |
|
198 | if (url.req) {
|
199 |
|
200 | if (!url.reqParamKeys) {
|
201 |
|
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 |
|
209 | return false;
|
210 | } else {
|
211 |
|
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 |
|
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 |
|
251 | url.template = _.template(url.fmt);
|
252 | }
|
253 |
|
254 | return url.template(
|
255 | _.transform(
|
256 | vars,
|
257 | function(note, val, name) {
|
258 |
|
259 | note[name] = encodeURIComponent(val);
|
260 |
|
261 | delete params[name];
|
262 | },
|
263 | {}
|
264 | )
|
265 | );
|
266 | }
|
267 |
|
268 | function exec(transport, spec, params, cb) {
|
269 | var request = {
|
270 | method: spec.method,
|
271 | };
|
272 | var query = {};
|
273 | var i;
|
274 |
|
275 |
|
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 |
|
298 | if (spec.needsBody && !params.body) {
|
299 | throw new TypeError('A request body is required.');
|
300 | }
|
301 |
|
302 |
|
303 | if (spec.bulkBody) {
|
304 | request.bulkBody = true;
|
305 | }
|
306 |
|
307 | if (spec.method === 'HEAD') {
|
308 | request.castExists = true;
|
309 | }
|
310 |
|
311 |
|
312 | if (spec.url) {
|
313 |
|
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 |
|
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 |
|
334 | if (!spec.paramKeys) {
|
335 |
|
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 |
|
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 |
|
406 | function commaSepList(str) {
|
407 | return str.split(',').map(function(i) {
|
408 | return i.trim();
|
409 | });
|
410 | }
|