10 |
11 | Name: {{name}}
12 |
13 |
14 |
Debug info
15 |
16 | $location.protocol() = {{$location.protocol()}}
17 | $location.host() = {{$location.host()}}
18 | $location.port() = {{$location.port()}}
19 | $location.path() = {{$location.path()}}
20 | $location.search() = {{$location.search()}}
21 | $location.hash() = {{$location.hash()}}
22 |
23 |
24 |
25 |
26 |
27 |
28 |
29 |
30 |
31 |
53 |
54 |
55 |
--------------------------------------------------------------------------------
/app/mock_data/people/pikachu.get.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "Pikachu"
3 | }
4 |
--------------------------------------------------------------------------------
/app/scripts/angular-apimock.js:
--------------------------------------------------------------------------------
1 | /* Create the main module, `apiMock`. It's the one that needs to be included in
2 | * your projects. E.g. `angular.module('myApp', ['apiMock'])`. You don't need
3 | * to do anything else, but you can configure the paths for api-calls and mock
4 | * data by calling `app.config(function (apiMockProvider) { ... });`.
5 | */
6 | angular.module('apiMock', [])
7 |
8 | .config(function ($httpProvider) {
9 | /* This is where the magic happens. Configure `$http` to use our
10 | `httpInterceptor` on all calls. It's what allows us to do automatic routing.
11 | */
12 | $httpProvider.interceptors.push('httpInterceptor');
13 | })
14 |
15 | .provider('apiMock', function () {
16 | /* This is the Provider for apiMock. It's used by `httpInterceptor` to support
17 | * mocking.
18 | *
19 | * Config options:
20 | * `mockDataPath` string: the path to be rerouted to. Default: `/mock_data`.
21 | * `apiPath` string: the path to be rerouted from. Default: `/api`.
22 | *
23 | * Public interface:
24 | * `onRequest` method: takes a `request` object and decides if mocking should
25 | * be done on this request. It checks global and local apiMock flags to see
26 | * if it should mock. It also checks the request URL if it starts with `apiPath`.
27 | * If the request is to have a `recover` attempt it's put in the fallbacks list.
28 | * A GET request to `/api/user/5?option=full` turns into `/mock_data/user/5.get.json`.
29 | * `onResponse` method: takes a `request` object and simply removes it from list
30 | * of fallbacks for `recover`.
31 | * `recover` method: if request has been marked for recover `onRequest` then it
32 | * will reroute to mock data. This is only to be called on response error.
33 | *
34 | * Private members:
35 | * `_countFallbacks` method: returns the current number of fallbacks in queue.
36 | * Only used for unit testing.
37 | */
38 |
39 | // Helper objects
40 | //
41 |
42 | var $location;
43 | var $log;
44 | var $q;
45 | var $filter;
46 | var config = {
47 | defaultMock: false,
48 | mockDataPath: '/mock_data',
49 | apiPath: '/api',
50 | disable: false,
51 | stripQueries: true,
52 | delay: 0
53 | };
54 | var fallbacks = [];
55 |
56 | // Helper methods
57 | //
58 |
59 | // TODO: IE8: remove when we drop IE8/Angular 1.2 support.
60 | // Object.keys isn't supported in IE8. Which we need to support as long as we support Angular 1.2.
61 | // This isn't a complete polyfill! It's just enough for what we need (and we don't need to bloat).
62 | function objectKeys(object) {
63 | var keys = [];
64 |
65 | angular.forEach(object, function (value, key) {
66 | keys.push(key);
67 | });
68 |
69 | return keys;
70 | }
71 |
72 | // TODO: IE8: remove when we drop IE8/Angular 1.2 support.
73 | // Date.prototype.toISOString isn't supported in IE8. Which we need to support as long as we support Angular 1.2.
74 | // Modified from MDN: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Date/toISOString
75 | function toISOString(date) {
76 | function pad(number) {
77 | if (number < 10) {
78 | return '0' + number;
79 | }
80 | return number;
81 | }
82 |
83 | return date.getUTCFullYear() +
84 | '-' + pad(date.getUTCMonth() + 1) +
85 | '-' + pad(date.getUTCDate()) +
86 | 'T' + pad(date.getUTCHours()) +
87 | '.' + pad(date.getUTCMinutes()) +
88 | '.' + pad(date.getUTCSeconds()) +
89 | '.' + (date.getUTCMilliseconds() / 1000).toFixed(3).slice(2, 5) +
90 | 'Z';
91 | }
92 |
93 | // Taken as-is from Angular 1.4.x: https://github.com/angular/angular.js/blob/f13852c179ffd9ec18b7a94df27dec39eb5f19fc/src/Angular.js#L296
94 | function forEachSorted(obj, iterator, context) {
95 | var keys = objectKeys(obj).sort();
96 | for (var i = 0; i < keys.length; i++) {
97 | iterator.call(context, obj[keys[i]], keys[i]);
98 | }
99 | return keys;
100 | }
101 |
102 | // Modified from Angular 1.4.x: https://github.com/angular/angular.js/blob/929ec6ba5a60e926654583033a90aebe716123c0/src/ng/http.js#L18
103 | function serializeValue(v) {
104 | if (angular.isDate(v)) {
105 | return toISOString(v);
106 | }
107 |
108 | return v;
109 | }
110 |
111 | // Modified from Angular 1.4.x: https://github.com/angular/angular.js/blob/720012eab6fef5e075a1d6876dd2e508c8e95b73/src/ngResource/resource.js#L405
112 | function encodeUriQuery(val) {
113 | return encodeURIComponent(val).
114 | replace(/%40/gi, '@').
115 | replace(/%3A/gi, ':').
116 | replace(/%24/g, '$').
117 | replace(/%2C/gi, ',').
118 | replace(/%20/g, '+');
119 | }
120 |
121 | // TODO: replace with a $httpParamSerializerJQLikeProvider() call when we require Angular 1.4 (i.e. when we drop 1.2 and 1.3).
122 | // Modified from Angular 1.4.x: https://github.com/angular/angular.js/blob/929ec6ba5a60e926654583033a90aebe716123c0/src/ng/http.js#L108
123 | function jQueryLikeParamSerializer(params) {
124 | var parts = [];
125 |
126 | function serialize(toSerialize, prefix, topLevel) {
127 | if (angular.isArray(toSerialize)) {
128 | // Serialize arrays.
129 | angular.forEach(toSerialize, function (value, index) {
130 | serialize(value, prefix + '[' + (angular.isObject(value) ? index : '') + ']');
131 | });
132 | } else if (angular.isObject(toSerialize) && !angular.isDate(toSerialize)) {
133 | // Serialize objects (not dates, because that's covered by the default case).
134 | forEachSorted(toSerialize, function (value, key) {
135 | serialize(value, prefix +
136 | (topLevel ? '' : '[') +
137 | key +
138 | (topLevel ? '' : ']'));
139 | });
140 | } else if (toSerialize === undefined || toSerialize === '') {
141 | // Keep empty parameters as it still affects the mock file path.
142 | parts.push(encodeUriQuery(prefix));
143 | } else {
144 | // Serialize everything else (including dates).
145 | parts.push(encodeUriQuery(prefix) + '=' + encodeUriQuery(serializeValue(toSerialize)));
146 | }
147 | }
148 |
149 | serialize(params, '', true);
150 | return parts.join('&');
151 | }
152 |
153 | function queryStringToObject(paramString) {
154 | if (!paramString) {
155 | return {};
156 | }
157 |
158 | var paramArray = paramString.split('&');
159 |
160 | var result = {};
161 | angular.forEach(paramArray, function (param) {
162 | param = param.split('=');
163 | result[param[0]] = param[1] || '';
164 | });
165 |
166 | return result;
167 | }
168 |
169 | function detectParameter(keys) {
170 | var regex = /apimock/i;
171 | var result;
172 |
173 | angular.forEach(keys, function (value, key) {
174 | if (regex.test(key)) {
175 | result = value;
176 | }
177 | });
178 |
179 | return result;
180 | }
181 |
182 | function localMock(req) {
183 | return detectParameter(req);
184 | }
185 |
186 | function globalMock() {
187 | return detectParameter($location.search());
188 | }
189 |
190 | function getParameter(req) {
191 | var mockValue = localMock(req);
192 | // Note: `false` is a valid option, so we can't use falsy-checks.
193 | if (mockValue === undefined) {
194 | mockValue = globalMock();
195 | }
196 | if (mockValue === undefined) {
197 | mockValue = config.defaultMock;
198 | }
199 |
200 | return mockValue;
201 | }
202 |
203 | function getCommand(mockValue) {
204 | // Depending how we got mockValue it might've been parsed into a type or not.
205 | switch ((mockValue || '').toString().toLowerCase()) {
206 | case '200':
207 | case '404':
208 | case '500':
209 | return { type: 'respond', value: parseInt(mockValue, 10) };
210 |
211 | case 'auto':
212 | return { type: 'recover' };
213 |
214 | case 'true':
215 | return { type: 'reroute' };
216 | }
217 |
218 | return { type: 'ignore' };
219 | }
220 |
221 |
222 | function httpStatusResponse(status) {
223 | var response = {
224 | status: status,
225 | headers: {
226 | 'Content-Type': 'text/html; charset=utf-8',
227 | 'Server': 'Angular ApiMock'
228 | }
229 | };
230 | $log.info('apiMock: mocking HTTP status to ' + status);
231 | return $q.reject(response);
232 | }
233 |
234 | function apiPathMatched(url, apiPath) {
235 | var match; // Lets initially assume undefined as no match
236 |
237 | if (angular.isArray(apiPath)) {
238 | angular.forEach(apiPath, function (path) {
239 | if (match) { return; } // Hack to skip more recursive calls if already matched
240 | var found = apiPathMatched(url, path);
241 | if (found) {
242 | match = found;
243 | }
244 | });
245 | }
246 | if (match) {
247 | return match;
248 | }
249 | if (apiPath instanceof RegExp) {
250 | if (apiPath.test(url)) {
251 | return apiPath;
252 | }
253 | }
254 | if ((url.toString().indexOf(apiPath) === 0)) {
255 | return apiPath;
256 | }
257 | return match;
258 | }
259 |
260 | function isApiPath(url) {
261 | return (apiPathMatched(url, config.apiPath) !== undefined);
262 | }
263 |
264 | function prepareFallback(req) {
265 | if (isApiPath(req.url)) {
266 | fallbacks.push(req);
267 | }
268 | }
269 |
270 | function removeFallback(res) {
271 | var startLength = fallbacks.length;
272 | fallbacks = $filter('filter')(fallbacks, {
273 | method: '!' + res.method,
274 | url: '!' + res.url
275 | }, true);
276 |
277 | return startLength > fallbacks.length;
278 | }
279 |
280 | function reroute(req) {
281 | if (!isApiPath(req.url)) {
282 | return req;
283 | }
284 |
285 | // replace apiPath with mockDataPath.
286 | var oldPath = req.url;
287 |
288 | var redirectedPath = req.url.replace(apiPathMatched(req.url, config.apiPath), config.mockDataPath);
289 |
290 | var split = redirectedPath.split('?');
291 | var newPath = split[0];
292 | var queries = split[1] || '';
293 |
294 | // query strings are stripped by default (like ?search=banana).
295 | if (!config.stripQueries) {
296 |
297 | //test if we have query params
298 | //if we do merge them on to the params object
299 | var queryParamsFromUrl = queryStringToObject(queries);
300 | var params = angular.extend(req.params || {}, queryParamsFromUrl);
301 |
302 | //test if there is already a trailing /
303 | if (newPath[newPath.length - 1] !== '/') {
304 | newPath += '/';
305 | }
306 |
307 | //serialize the param object to convert to string
308 | //and concatenate to the newPath
309 | newPath += angular.lowercase(jQueryLikeParamSerializer(params));
310 | }
311 |
312 | //Kill the params property so they aren't added back on to the end of the url
313 | req.params = undefined;
314 |
315 | // add file endings (method verb and .json).
316 | if (newPath[newPath.length - 1] === '/') {
317 | newPath = newPath.slice(0, -1);
318 | }
319 | newPath += '.' + req.method.toLowerCase() + '.json';
320 |
321 | req.method = 'GET';
322 | req.url = newPath;
323 | $log.info('apiMock: rerouting ' + oldPath + ' to ' + newPath);
324 |
325 | return req;
326 | }
327 |
328 | // Expose public interface for provider instance
329 | //
330 |
331 | function ApiMock(_$location, _$log, _$q, _$filter) {
332 | $location = _$location;
333 | $log = _$log;
334 | $q = _$q;
335 | $filter = _$filter;
336 | }
337 |
338 | var p = ApiMock.prototype;
339 |
340 | p._countFallbacks = function () {
341 | return fallbacks.length;
342 | };
343 |
344 | p.getDelay = function () {
345 | return config.delay;
346 | };
347 |
348 | p.onRequest = function (req) {
349 | if (config.disable) {
350 | return req;
351 | }
352 |
353 | var param = getParameter(req);
354 | var command = getCommand(param);
355 |
356 | switch (command.type) {
357 | case 'reroute':
358 | return reroute(req);
359 | case 'recover':
360 | prepareFallback(req);
361 | return req;
362 | case 'respond':
363 | return httpStatusResponse(command.value);
364 | case 'ignore':
365 | /* falls through */
366 | default:
367 | return req;
368 | }
369 | };
370 |
371 | p.onResponse = function (res) {
372 | removeFallback(res);
373 | return res;
374 | };
375 |
376 | p.recover = function (rej) {
377 | if (config.disable) {
378 | return false;
379 | }
380 |
381 | if (rej.config === undefined) {// Why is this called with regular response object sometimes?
382 | return false;
383 | }
384 |
385 | if (removeFallback(rej.config)) {
386 | $log.info('apiMock: recovering from failure at ' + rej.config.url);
387 | return reroute(rej.config);
388 | }
389 |
390 | return false;
391 | };
392 |
393 | // Expose Provider interface
394 | //
395 |
396 | this.config = function (options) {
397 | angular.extend(config, options);
398 | };
399 |
400 | this.$get = function ($location, $log, $q, $filter) {
401 | return new ApiMock($location, $log, $q, $filter);
402 | };
403 | })
404 |
405 | .service('httpInterceptor', function ($injector, $q, $timeout, apiMock) {
406 | /* The main service. Is jacked in as a interceptor on `$http` so it gets called
407 | * on every http call. This allows us to do our magic. It uses the provider
408 | * `apiMock` to determine if a mock should be done, then do the actual mocking.
409 | */
410 | this.request = function (req) {
411 | return apiMock.onRequest(req);
412 | };
413 |
414 | this.response = function (res) {
415 | var deferred = $q.defer();
416 |
417 | $timeout(
418 | function () {
419 | // TODO: Apparently, no tests break regardless what this resolves to. Fix the tests!
420 | deferred.resolve(apiMock.onResponse(res));
421 | },
422 | apiMock.getDelay(),
423 | true // Trigger a $digest.
424 | );
425 |
426 | return deferred.promise;
427 | };
428 |
429 | this.responseError = function (rej) {
430 | var deferred = $q.defer();
431 |
432 | $timeout(
433 | function () {
434 | var recover = apiMock.recover(rej);
435 |
436 | if (recover) {
437 | var $http = $injector.get('$http');
438 | $http(recover).then(function (data) {
439 | deferred.resolve(data);
440 | });
441 | } else {
442 | deferred.reject(rej);
443 | }
444 | },
445 | apiMock.getDelay(),
446 | true // Trigger a $digest.
447 | );
448 |
449 | return deferred.promise;
450 | };
451 | });
452 |
--------------------------------------------------------------------------------
/bower.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "angular-apimock",
3 | "version": "0.3.3",
4 | "description": "Automatically route your API calls to static JSON files, for hiccup free front–end development.",
5 | "authors": [ "John-Philip Johansson