UNPKG

can

Version:

MIT-licensed, client-side, JavaScript framework that makes building rich web applications easy.

764 lines (692 loc) 22.2 kB
// # can/util/fixture.js // // Intercepts AJAX requests and simulates them with either a function or a // file. This is used to develop independently from backend services. steal('can/util', 'can/util/string', 'can/util/object', function (can) { // can.fixture relies on can.Object in order to work and needs to be // included before can.fixture in order to use it, otherwise it'll error. if (!can.Object) { throw new Error('can.fixture depends on can.Object. Please include it before can.fixture.'); } // Get the URL from old Steal root, new Steal config or can.fixture.rootUrl var getUrl = function (url) { if (typeof steal !== 'undefined') { // New steal // TODO The correct way to make this work with new Steal is to change getUrl // to return a deferred and have the other code accept a deferred. if(steal.joinURIs) { var base = steal.config("baseUrl"); var joined = steal.joinURIs(base, url); return joined; } // Legacy steal if (can.isFunction(steal.config)) { if (steal.System) { return steal.joinURIs(steal.config('baseURL'), url); } else { return steal.config() .root.mapJoin(url) .toString(); } } return steal.root.join(url) .toString(); } return (can.fixture.rootUrl || '') + url; }; // Manipulates the AJAX prefilter to identify whether or not we should // manipulate the AJAX call to change the URL to a static file or call // a function for a dynamic fixture. var updateSettings = function (settings, originalOptions) { if (!can.fixture.on || settings.fixture === false) { return; } // A simple wrapper for logging fixture.js. var log = function () { //!steal-remove-start can.dev.log('can/fixture/fixture.js: ' + Array.prototype.slice.call(arguments) .join(' ')); //!steal-remove-end }; // We always need the type which can also be called method, default to GET settings.type = settings.type || settings.method || 'GET'; // add the fixture option if programmed in var data = overwrite(settings); // If there is not a fixture for this AJAX request, do nothing. if (!settings.fixture) { if (window.location.protocol === "file:") { log("ajax request to " + settings.url + ", no fixture found"); } return; } // If the fixture already exists on can.fixture, update the fixture option if (typeof settings.fixture === "string" && can.fixture[settings.fixture]) { settings.fixture = can.fixture[settings.fixture]; } // If the fixture setting is a string, we just change the URL of the // AJAX call to the fixture URL. if (typeof settings.fixture === "string") { var url = settings.fixture; // If the URL starts with //, we need to update the URL to become // the full path. if (/^\/\//.test(url)) { // this lets us use rootUrl w/o having steal... url = getUrl(settings.fixture.substr(2)); } if (data) { // Template static fixture URLs url = can.sub(url, data); } delete settings.fixture; //!steal-remove-start log("looking for fixture in " + url); //!steal-remove-end // Override the AJAX settings, changing the URL to the fixture file, // removing the data, and changing the type to GET. settings.url = url; settings.data = null; settings.type = "GET"; if (!settings.error) { // If no error handling is provided, we provide one and throw an // error. settings.error = function (xhr, error, message) { throw new Error("fixtures.js Error " + error + " " + message); }; } // Otherwise, it is a function and we add the fixture data type so the // fixture transport will handle it. } else { //!steal-remove-start log("using a dynamic fixture for " + settings.type + " " + settings.url); //!steal-remove-end // TODO: make everything go here for timing and other fun stuff // add to settings data from fixture ... if (settings.dataTypes) { settings.dataTypes.splice(0, 0, "fixture"); } if (data && originalOptions) { originalOptions.data = originalOptions.data || {}; can.extend(originalOptions.data, data); } } }, // A helper function that takes what's called with response // and moves some common args around to make it easier to call extractResponse = function (status, statusText, responses, headers) { // if we get response(RESPONSES, HEADERS) if (typeof status !== "number") { headers = statusText; responses = status; statusText = "success"; status = 200; } // if we get response(200, RESPONSES, HEADERS) if (typeof statusText !== "string") { headers = responses; responses = statusText; statusText = "success"; } if (status >= 400 && status <= 599) { this.dataType = "text"; } return [status, statusText, extractResponses(this, responses), headers]; }, // If we get data instead of responses, make sure we provide a response // type that matches the first datatype (typically JSON) extractResponses = function (settings, responses) { var next = settings.dataTypes ? settings.dataTypes[0] : (settings.dataType || 'json'); if (!responses || !responses[next]) { var tmp = {}; tmp[next] = responses; responses = tmp; } return responses; }; // Set up prefiltering and transmission handling in order to actually power // can.fixture. This is handled two different ways, depending on whether or // not CanJS is using jQuery or not. // If we are using jQuery, we have access to ajaxPrefilter and ajaxTransport if (can.ajaxPrefilter && can.ajaxTransport) { // the pre-filter needs to re-route the url can.ajaxPrefilter(updateSettings); can.ajaxTransport("fixture", function (s, original) { // remove the fixture from the datatype s.dataTypes.shift(); //we'll return the result of the next data type var timeout, stopped = false; return { send: function (headers, callback) { // we'll immediately wait the delay time for all fixtures timeout = setTimeout(function () { // if the user wants to call success on their own, we allow it ... var success = function () { if (stopped === false) { callback.apply(null, extractResponse.apply(s, arguments)); } }, // get the result form the fixture result = s.fixture(original, success, headers, s); if (result !== undefined) { // Run the callback as a 200 success and with the results with the correct dataType callback(200, "success", extractResponses(s, result), {}); } }, can.fixture.delay); }, abort: function () { stopped = true; clearTimeout(timeout); } }; }); // If we are not using jQuery, we don't have access to those nice ajaxPrefilter // and ajaxTransport functions, so we need to monkey patch can.ajax. } else { var AJAX = can.ajax; can.ajax = function (settings) { updateSettings(settings, settings); // If the call is a fixture call, we run the same type of code as we would // with jQuery's ajaxTransport. if (settings.fixture) { var timeout, deferred = new can.Deferred(), stopped = false; //TODO this should work with response deferred.getResponseHeader = function () {}; // Call success or fail after deferred resolves deferred.then(settings.success, settings.fail); // Abort should stop the timeout and calling the success callback deferred.abort = function () { clearTimeout(timeout); stopped = true; deferred.reject(deferred); }; // set a timeout that simulates making a request .... timeout = setTimeout(function () { // if the user wants to call success on their own, we allow it ... var success = function () { var response = extractResponse.apply(settings, arguments), status = response[0]; if ((status >= 200 && status < 300 || status === 304) && stopped === false) { deferred.resolve(response[2][settings.dataType]); } else { // TODO probably resolve better deferred.reject(deferred, 'error', response[1]); } }, // Get the results from the fixture. result = settings.fixture(settings, success, settings.headers, settings); if (result !== undefined) { // Resolve with fixture results deferred.resolve(result); } }, can.fixture.delay); return deferred; // Otherwise just run a normal can.ajax call. } else { return AJAX(settings); } }; } // A list of 'overwrite' settings objects var overwrites = [], // Finds and returns the index of an overwrite function find = function (settings, exact) { for (var i = 0; i < overwrites.length; i++) { if ($fixture._similar(settings, overwrites[i], exact)) { return i; } } return -1; }, // Overwrites the settings fixture if an overwrite matches overwrite = function (settings) { var index = find(settings); if (index > -1) { settings.fixture = overwrites[index].fixture; return $fixture._getData(overwrites[index].url, settings.url); } }, // Attemps to guess where the id is in an AJAX call's URL and returns it. getId = function (settings) { var id = settings.data.id; if (id === undefined && typeof settings.data === "number") { id = settings.data; } // Parses the URL looking for all digits if (id === undefined) { // Set id equal to the value settings.url.replace(/\/(\d+)(\/|$|\.)/g, function (all, num) { id = num; }); } if (id === undefined) { // If that doesn't work Parses the URL looking for all words id = settings.url.replace(/\/(\w+)(\/|$|\.)/g, function (all, num) { // As long as num isn't the word "update", set id equal to the value if (num !== 'update') { id = num; } }); } if (id === undefined) { // If id is still not set, a random number is guessed. id = Math.round(Math.random() * 1000); } return id; }; // ## can.fixture // Simulates AJAX requests. var $fixture = can.fixture = function (settings, fixture) { // If fixture is provided, set up a new fixture. if (fixture !== undefined) { if (typeof settings === 'string') { // Match URL if it has GET, POST, PUT, or DELETE. var matches = settings.match(/(GET|POST|PUT|DELETE) (.+)/i); // If not, we don't set the type, which eventually defaults to GET if (!matches) { settings = { url: settings }; // If it does match, we split the URL in half and create an object with // each half as the url and type properties. } else { settings = { url: matches[2], type: matches[1] }; } } // Check if the same fixture was previously added, if so, we remove it // from our array of fixture overwrites. var index = find(settings, !! fixture); if (index > -1) { overwrites.splice(index, 1); } if (fixture == null) { return; } settings.fixture = fixture; overwrites.push(settings); // If a fixture isn't provided, we assume that settings is // an array of fixtures, and we should iterate over it, and set up // the new fixtures. } else { can.each(settings, function (fixture, url) { $fixture(url, fixture); }); } }; var replacer = can.replacer; can.extend(can.fixture, { // Find an overwrite, given some ajax settings. _similar: function (settings, overwrite, exact) { if (exact) { return can.Object.same(settings, overwrite, { fixture: null }); } else { return can.Object.subset(settings, overwrite, can.fixture._compare); } }, // Comparator object used to find a similar overwrite. _compare: { url: function (a, b) { return !!$fixture._getData(b, a); }, fixture: null, type: "i" }, // Returns data from a url, given a fixtue URL. For example, given // "todo/{id}" and "todo/5", it will return an object with an id property // equal to 5. _getData: function (fixtureUrl, url) { var order = [], // Sanitizes fixture URL fixtureUrlAdjusted = fixtureUrl.replace('.', '\\.') .replace('?', '\\?'), // Creates a regular expression out of the adjusted fixture URL and // runs it on the URL we passed in. res = new RegExp(fixtureUrlAdjusted.replace(replacer, function (whole, part) { order.push(part); return "([^\/]+)"; }) + "$") .exec(url), data = {}; // If there were no matches, return null; if (!res) { return null; } // Shift off the URL and just keep the data. res.shift(); can.each(order, function (name) { // Add data from regular expression onto data object. data[name] = res.shift(); }); return data; }, // ## can.fixture.store // Make a store of objects to use when making requests against fixtures. store: function (count, make, filter) { /*jshint eqeqeq:false */ // the currentId to use when a new instance is created. var currentId = 0, findOne = function (id) { for (var i = 0; i < items.length; i++) { if (id == items[i].id) { return items[i]; } } }, methods = {}, types, items, reset; if(can.isArray(count) && typeof count[0] === "string" ){ types = count; count = make; make= filter; filter = arguments[3]; } else if(typeof count === "string") { types = [count + "s", count]; count = make; make= filter; filter = arguments[3]; } if(typeof count === "number") { items = []; reset = function () { items = []; for (var i = 0; i < (count); i++) { //call back provided make var item = make(i, items); if (!item.id) { item.id = i; } currentId = Math.max(item.id + 1, currentId + 1) || items.length; items.push(item); } if (can.isArray(types)) { can.fixture["~" + types[0]] = items; can.fixture["-" + types[0]] = methods.findAll; can.fixture["-" + types[1]] = methods.findOne; can.fixture["-" + types[1] + "Update"] = methods.update; can.fixture["-" + types[1] + "Destroy"] = methods.destroy; can.fixture["-" + types[1] + "Create"] = methods.create; } }; } else { filter = make; var initialItems = count; reset = function(){ items = initialItems.slice(0); }; } // make all items can.extend(methods, { findAll: function (request) { request = request || {}; //copy array of items var retArr = items.slice(0); request.data = request.data || {}; //sort using order //order looks like ["age ASC","gender DESC"] can.each((request.data.order || []) .slice(0) .reverse(), function (name) { var split = name.split(" "); retArr = retArr.sort(function (a, b) { if (split[1].toUpperCase() !== "ASC") { if (a[split[0]] < b[split[0]]) { return 1; } else if (a[split[0]] === b[split[0]]) { return 0; } else { return -1; } } else { if (a[split[0]] < b[split[0]]) { return -1; } else if (a[split[0]] === b[split[0]]) { return 0; } else { return 1; } } }); }); //group is just like a sort can.each((request.data.group || []) .slice(0) .reverse(), function (name) { var split = name.split(" "); retArr = retArr.sort(function (a, b) { return a[split[0]] > b[split[0]]; }); }); var offset = parseInt(request.data.offset, 10) || 0, limit = parseInt(request.data.limit, 10) || (items.length - offset), i = 0; //filter results if someone added an attr like parentId for (var param in request.data) { i = 0; if (request.data[param] !== undefined && // don't do this if the value of the param is null (ignore it) (param.indexOf("Id") !== -1 || param.indexOf("_id") !== -1)) { while (i < retArr.length) { if (request.data[param] != retArr[i][param]) { // jshint eqeqeq: false retArr.splice(i, 1); } else { i++; } } } } if ( typeof filter === "function" ) { i = 0; while (i < retArr.length) { if (!filter(retArr[i], request)) { retArr.splice(i, 1); } else { i++; } } } else if( typeof filter === "object" ) { i = 0; while (i < retArr.length) { if ( !can.Object.subset(retArr[i], request.data, filter) ) { retArr.splice(i, 1); } else { i++; } } } // Return the data spliced with limit and offset, along with related values // (e.g. count, limit, offset) return { "count": retArr.length, "limit": request.data.limit, "offset": request.data.offset, "data": retArr.slice(offset, offset + limit) }; }, /** * @description Simulate a findOne request on a fixture. * @function can.fixture.types.Store.findOne * @parent can.fixture.types.Store * @signature `store.findOne(request, response)` * @param {Object} request Parameters for the request. * @param {Function} response A function to call with the retrieved item. * * @body * `store.findOne(request, response(item))` simulates a request to * get a single item from the server by id. * * todosStore.findOne({ * url: "/todos/5" * }, function(todo){ * * }); * */ findOne: function (request, response) { var item = findOne(getId(request)); if(typeof item === "undefined") { return response(404, 'Requested resource not found'); } response(item); }, // ## fixtureStore.update // Simulates a can.Model.update to a fixture update: function (request, response) { var id = getId(request), item = findOne(id); if(typeof item === "undefined") { return response(404, 'Requested resource not found'); } // TODO: make it work with non-linear ids .. can.extend(item, request.data); response({ id: id }, { location: request.url || "/" + getId(request) }); }, /** * @description Simulate destroying a Model on a fixture. * @function can.fixture.types.Store.destroy * @parent can.fixture.types.Store * @signature `store.destroy(request, callback)` * @param {Object} request Parameters for the request. * @param {Function} callback A function to call after destruction. * * @body * `store.destroy(request, response())` simulates * a request to destroy an item from the server. * * ``` * todosStore.destroy({ * url: "/todos/5" * }, function(){}); * ``` */ destroy: function (request, response) { var id = getId(request), item = findOne(id); if(typeof item === "undefined") { return response(404, 'Requested resource not found'); } for (var i = 0; i < items.length; i++) { if (items[i].id == id) { // jshint eqeqeq: false items.splice(i, 1); break; } } // TODO: make it work with non-linear ids .. return {}; }, // ## fixtureStore.create // Simulates a can.Model.create to a fixture create: function (settings, response) { var item = typeof make === 'function' ? make(items.length, items) : {}; can.extend(item, settings.data); // If an ID wasn't passed into the request, we give the item // a unique ID. if (!item.id) { item.id = currentId++; } // Push the new item into the store. items.push(item); response({ id: item.id }, { location: settings.url + "/" + item.id }); } }); reset(); // if we have types given add them to can.fixture return can.extend({ getId: getId, find: function (settings) { return findOne(getId(settings)); }, reset: reset }, methods); }, rand: function randomize(arr, min, max) { if (typeof arr === 'number') { if (typeof min === 'number') { return arr + Math.floor(Math.random() * (min - arr)); } else { return Math.floor(Math.random() * arr); } } var rand = randomize; // get a random set if (min === undefined) { return rand(arr, rand(arr.length + 1)); } // get a random selection of arr var res = []; arr = arr.slice(0); // set max if (!max) { max = min; } //random max max = min + Math.round(rand(max - min)); for (var i = 0; i < max; i++) { res.push(arr.splice(rand(arr.length), 1)[0]); } return res; }, xhr: function (xhr) { return can.extend({}, { abort: can.noop, getAllResponseHeaders: function () { return ""; }, getResponseHeader: function () { return ""; }, open: can.noop, overrideMimeType: can.noop, readyState: 4, responseText: "", responseXML: null, send: can.noop, setRequestHeader: can.noop, status: 200, statusText: "OK" }, xhr); }, on: true }); // ## can.fixture.delay // The delay, in milliseconds, between an AJAX request being made and when // the success callback gets called. can.fixture.delay = 200; // ## can.fixture.rootUrl // The root URL which fixtures will use. can.fixture.rootUrl = getUrl(''); can.fixture["-handleFunction"] = function (settings) { if (typeof settings.fixture === "string" && can.fixture[settings.fixture]) { settings.fixture = can.fixture[settings.fixture]; } if (typeof settings.fixture === "function") { setTimeout(function () { if (settings.success) { settings.success.apply(null, settings.fixture(settings, "success")); } if (settings.complete) { settings.complete.apply(null, settings.fixture(settings, "complete")); } }, can.fixture.delay); return true; } return false; }; //Expose this for fixture debugging can.fixture.overwrites = overwrites; can.fixture.make = can.fixture.store; return can.fixture; });