Jump To …

can.fixture.js

/*!
* CanJS - 1.1.4 (2013-02-05)
* http://canjs.us/
* Copyright (c) 2013 Bitovi
* Licensed MIT
*/
(function (can, window, undefined) {

can/util/object/object.js

  var isArray = can.isArray,

essentially returns an object that has all the must have comparisons ... must haves, do not return true when provided undefined

    cleanSet = function (obj, compares) {
      var copy = can.extend({}, obj);
      for (var prop in copy) {
        var compare = compares[prop] === undefined ? compares["*"] : compares[prop];
        if (same(copy[prop], undefined, compare)) {
          delete copy[prop]
        }
      }
      return copy;
    },
    propCount = function (obj) {
      var count = 0;
      for (var prop in obj) count++;
      return count;
    };

  can.Object = {};

  var same = can.Object.same = function (a, b, compares, aParent, bParent, deep) {
    var aType = typeof a,
      aArray = isArray(a),
      comparesType = typeof compares,
      compare;

    if (comparesType == 'string' || compares === null) {
      compares = compareMethods[compares];
      comparesType = 'function'
    }
    if (comparesType == 'function') {
      return compares(a, b, aParent, bParent)
    }
    compares = compares || {};

    if (a instanceof Date) {
      return a === b;
    }
    if (deep === -1) {
      return aType === 'object' || a === b;
    }
    if (aType !== typeof b || aArray !== isArray(b)) {
      return false;
    }
    if (a === b) {
      return true;
    }
    if (aArray) {
      if (a.length !== b.length) {
        return false;
      }
      for (var i = 0; i < a.length; i++) {
        compare = compares[i] === undefined ? compares["*"] : compares[i]
        if (!same(a[i], b[i], a, b, compare)) {
          return false;
        }
      };
      return true;
    } else if (aType === "object" || aType === 'function') {
      var bCopy = can.extend({}, b);
      for (var prop in a) {
        compare = compares[prop] === undefined ? compares["*"] : compares[prop];
        if (!same(a[prop], b[prop], compare, a, b, deep === false ? -1 : undefined)) {
          return false;
        }
        delete bCopy[prop];
      }

go through bCopy props ... if there is no compare .. return false

      for (prop in bCopy) {
        if (compares[prop] === undefined || !same(undefined, b[prop], compares[prop], a, b, deep === false ? -1 : undefined)) {
          return false;
        }
      }
      return true;
    }
    return false;
  };

  can.Object.subsets = function (checkSet, sets, compares) {
    var len = sets.length,
      subsets = [],
      checkPropCount = propCount(checkSet),
      setLength;

    for (var i = 0; i < len; i++) {

check this subset

      var set = sets[i];
      if (can.Object.subset(checkSet, set, compares)) {
        subsets.push(set)
      }
    }
    return subsets;
  };

  can.Object.subset = function (subset, set, compares) {

go through set {type: 'folder'} and make sure every property is in subset {type: 'folder', parentId :5} then make sure that set has fewer properties make sure we are only checking 'important' properties in subset (ones that have to have a value)

    var setPropCount = 0,
      compares = compares || {};

    for (var prop in set) {

      if (!same(subset[prop], set[prop], compares[prop], subset, set)) {
        return false;
      }
    }
    return true;
  }

  var compareMethods = {
    "null": function () {
      return true;
    },
    i: function (a, b) {
      return ("" + a).toLowerCase() == ("" + b).toLowerCase()
    }
  }

can/util/fixture/fixture.js

Get the URL from old Steal root, new Steal config or can.fixture.rootUrl

  var getUrl = function (url) {
    if (typeof steal !== 'undefined') {
      if (can.isFunction(steal.config)) {
        return steal.config().root.mapJoin(url).toString();
      }
      return steal.root.join(url).toString();
    }
    return (can.fixture.rootUrl || '') + url;
  }

  var updateSettings = function (settings, originalOptions) {
    if (!can.fixture.on) {
      return;
    }

simple wrapper for logging

    var _logger = function (type, arr) {
      if (console.log.apply) {
        Function.prototype.call.apply(console[type], [console].concat(arr));

console[type].apply(console, arr)

      } else {
        console[type](arr)
      }
    },
      log = function () {
        if (window.console && console.log) {
          Array.prototype.unshift.call(arguments, 'fixture INFO:');
          _logger("log", Array.prototype.slice.call(arguments));
        }
        else if (window.opera && window.opera.postError) {
          opera.postError("fixture INFO: " + Array.prototype.join.call(arguments, ','));
        }
      }

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 we don't have a fixture, do nothing

    if (!settings.fixture) {
      if (window.location.protocol === "file:") {
        log("ajax request to " + settings.url + ", no fixture found");
      }
      return;
    }

if referencing something else, update the fixture option

    if (typeof settings.fixture === "string" && can.fixture[settings.fixture]) {
      settings.fixture = can.fixture[settings.fixture];
    }

if a string, we just point to the right url

    if (typeof settings.fixture == "string") {
      var url = settings.fixture;

      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;



      settings.url = url;
      settings.data = null;
      settings.type = "GET";
      if (!settings.error) {
        settings.error = function (xhr, error, message) {
          throw "fixtures.js Error " + error + " " + message;
        };
      }
    }
    else {

it's a function ... add the fixture datatype so our fixture transport handles it TODO: make everything go here for timing and other fun stuff add to settings data from fixture ...

      settings.dataTypes && settings.dataTypes.splice(0, 0, "fixture");

      if (data && originalOptions) {
        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;
    };

used to check urls check if jQuery

  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) {

make sure the result has the right dataType

              callback(200, "success", extractResponses(s, result), {});
            }
          }, can.fixture.delay);
        },
        abort: function () {
          stopped = true;
          clearTimeout(timeout)
        }
      };
    });
  } else {
    var AJAX = can.ajax;
    can.ajax = function (settings) {
      updateSettings(settings, settings);
      if (settings.fixture) {
        var timeout, d = new can.Deferred(),
          stopped = false;

TODO this should work with response

        d.getResponseHeader = function () {}

call success and fail

        d.then(settings.success, settings.fail);

abort should stop the timeout and calling success

        d.abort = function () {
          clearTimeout(timeout);
          stopped = true;
          d.reject(d)
        }

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) {
              d.resolve(response[2][settings.dataType])
            } else {

TODO probably resolve better

              d.reject(d, 'error', response[1]);
            }
          },

get the result form the fixture

            result = settings.fixture(settings, success, settings.headers, settings);
          if (result !== undefined) {
            d.resolve(result)
          }
        }, can.fixture.delay);

        return d;
      } else {
        return AJAX(settings);
      }
    }
  }

  var typeTest = /^(script|json|text|jsonp)$/,

a list of 'overwrite' settings object

    overwrites = [],

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)
      }

    },

Makes an attempt to guess where the id is at in the url and returns it.

    getId = function (settings) {
      var id = settings.data.id;

      if (id === undefined && typeof settings.data === "number") {
        id = settings.data;
      }



      if (id === undefined) {
        settings.url.replace(/\/(\d+)(\/|$|\.)/g, function (all, num) {
          id = num;
        });
      }

      if (id === undefined) {
        id = settings.url.replace(/\/(\w+)(\/|$|\.)/g, function (all, num) {
          if (num != 'update') {
            id = num;
          }
        })
      }

      if (id === undefined) { // if still not set, guess a random number
        id = Math.round(Math.random() * 1000)
      }

      return id;
    };

  var $fixture = can.fixture = function (settings, fixture) {

if we provide a fixture ...

    if (fixture !== undefined) {
      if (typeof settings == 'string') {

handle url strings

        var matches = settings.match(/(GET|POST|PUT|DELETE) (.+)/i);
        if (!matches) {
          settings = {
            url: settings
          };
        } else {
          settings = {
            url: matches[2],
            type: matches[1]
          };
        }

      }

handle removing. An exact match if fixture was provided, otherwise, anything similar

      var index = find(settings, !! fixture);
      if (index > -1) {
        overwrites.splice(index, 1)
      }
      if (fixture == null) {
        return
      }
      settings.fixture = fixture;
      overwrites.push(settings)
    } else {
      can.each(settings, function (fixture, url) {
        $fixture(url, fixture);
      })
    }
  };
  var replacer = can.replacer;

  can.extend(can.fixture, {

given ajax settings, find an overwrite

    _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)
      }
    },
    _compare: {
      url: function (a, b) {
        return !!$fixture._getData(b, a)
      },
      fixture: null,
      type: "i"
    },

gets data from a url like "/todo/{id}" given "todo/5"

    _getData: function (fixtureUrl, url) {
      var order = [],
        fixtureUrlAdjusted = fixtureUrl.replace('.', '\\.').replace('?', '\\?'),
        res = new RegExp(fixtureUrlAdjusted.replace(replacer, function (whole, part) {
          order.push(part)
          return "([^\/]+)"
        }) + "$").exec(url),
        data = {};

      if (!res) {
        return null;
      }
      res.shift();
      can.each(order, function (name) {
        data[name] = res.shift()
      })
      return data;
    },

    store: function (types, count, make, filter) {

      var items = [],

TODO: change this to a hash

        findOne = function (id) {
          for (var i = 0; i < items.length; i++) {
            if (id == items[i].id) {
              return items[i];
            }
          }
        },
        methods = {};

      if (typeof types === "string") {
        types = [types + "s", types]
      } else if (!can.isArray(types)) {
        filter = make;
        make = count;
        count = types;
      }

make all items

      can.extend(methods, {

        findAll: function (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]) {
                  retArr.splice(i, 1);
                } else {
                  i++;
                }
              }
            }
          }

          if (filter) {
            i = 0;
            while (i < retArr.length) {
              if (!filter(retArr[i], request)) {
                retArr.splice(i, 1);
              } else {
                i++;
              }
            }
          }

return data spliced with limit and offset

          return {
            "count": retArr.length,
            "limit": request.data.limit,
            "offset": request.data.offset,
            "data": retArr.slice(offset, offset + limit)
          };
        },

        findOne: function (request, response) {
          var item = findOne(getId(request));
          response(item ? item : undefined);
        },

        update: function (request, response) {
          var id = getId(request);

TODO: make it work with non-linear ids ..

          can.extend(findOne(id), request.data);
          response({
            id: getId(request)
          }, {
            location: request.url || "/" + getId(request)
          });
        },

        destroy: function (request) {
          var id = getId(request);
          for (var i = 0; i < items.length; i++) {
            if (items[i].id == id) {
              items.splice(i, 1);
              break;
            }
          }

TODO: make it work with non-linear ids ..

          can.extend(findOne(id) || {}, request.data);
          return {};
        },

        create: function (settings, response) {
          var item = make(items.length, items);

          can.extend(item, settings.data);

          if (!item.id) {
            item.id = items.length;
          }

          items.push(item);
          var id = item.id || parseInt(Math.random() * 100000, 10);
          response({
            id: id
          }, {
            location: settings.url + "/" + id
          })
        }
      });

      var 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;
          }
          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;
        }
      }
      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 (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 = arguments.callee;

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 = 200;


  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;

})(can, this);