<!DOCTYPE html>
<html>

  <head>
    <link rel="stylesheet" type="text/css" href="https://cdnjs.cloudflare.com/ajax/libs/jasmine/2.2.1/jasmine.css">
    
    <script type="text/javascript" src="https://cdnjs.cloudflare.com/ajax/libs/jasmine/2.2.1/jasmine.js"></script>
    <script type="text/javascript" src="https://cdnjs.cloudflare.com/ajax/libs/jasmine/2.2.1/jasmine-html.js"></script>
    <script type="text/javascript" src="https://cdnjs.cloudflare.com/ajax/libs/jasmine/2.2.1/boot.js"></script>
    
    <script src="Accessor.js"></script>
    <!-- <script>window.accessor = new Accessor(); console.log(accessor);</script> -->
    <script src="spec.js"></script>
  </head>

  <body></body>

</html>
/* jshint jasmine: true */

'use strict';

describe("Accessor", function() {
  
  var a;
  
  beforeEach(function() {
    a = new Accessor();
  });
  
  describe('constructor', function() {
    it('should return a function', function() {
      expect(typeof a).toBe('function');
    });
    
    it('should set the initial value', function() {
      a = new Accessor('val');
      expect(a()).toBe('val');
    });
  });
  
  describe('fn', function() {
    it('should call set if an argument is passed', function() {
      spyOn(a, 'set');
      a(10);
      expect(a.set).toHaveBeenCalledWith(10);
    });
    it('should call get if no argument is passed', function() {
      spyOn(a, 'get');
      a();
      expect(a.get).toHaveBeenCalledWith();
    });
  });

  describe('get', function() {
    it('should return the stored value', function() {
      a._current = 'asdf';
      expect(a.get()).toBe('asdf');
    });
    it('should be overridable', function() {
      a._current = 1000;
      a.get = function() {};
      expect(a.get()).not.toBeDefined();
      expect(a()).not.toBeDefined();
    });
  });

  describe('set', function() {
    it('should set the stored value', function() {
      a.set(10);
      expect(a._current).toBe(10);
    });
    it('should be overridable', function() {
      a(1000);
      a.set = function () {};
      a(0);
      a.set(500);
      expect(a()).toBe(1000);
    });
  });

  describe('read-only', function() {
    beforeEach(function() {
      a = new Accessor("read-only", {write: false});
    });
    
    it('should be able to disable writing', function() {
      a('write');
      expect(a()).toBe('read-only');
    });
    
    it('should be able to force writing by passing force=true to set', function() {
      a.set('written', true);
      expect(a()).toBe('written');
    });
    
    it('should not be able to force writing by passing force=true to fn', function() {
      a('written', true);
      expect(a()).toBe('read-only');
    });
  });

  describe('write-only', function() {
    beforeEach(function() {
      a = new Accessor('write-only', {read: false});
    });
    
    it('should be able to disable reading', function() {
      expect(a._current).toBe('write-only');
      expect(a()).not.toBeDefined();
      expect(a.get()).not.toBeDefined();
    });
    
    it('should be able to force reading by passing force=true to get', function() {
      expect(a.get(true)).toBe('write-only');
    });
  });
  
  describe('changed', function() {
    it('should return true when a new value is set', function() {
      expect(a.changed()).toBe(false);
      a(1);
      expect(a.changed()).toBe(true);
    })
  });
  
  describe('update', function() {
    it('should set changed to false', function() {
      a(1);
      expect(a.changed()).toBe(true);
      a.update();
      expect(a.changed()).toBe(false);
    });
    
    it('should not affect the stored value', function() {
      a(1).update();
      expect(a()).toBe(1);
    });
  });
  
  describe('reset', function () {
    it('should set changed to false', function() {
      a(1);
      expect(a.changed()).toBe(true);
      a.update();
      expect(a.changed()).toBe(false);
    });
    it('should revert to the previous (or initial) stored value', function() {
      a(1).reset();
      expect(a()).not.toBeDefined();
    });
  });
  
  describe('toJSON', function() {
    it('should return the stored value', function() {
      expect(a(10).toJSON()).toBe(10);
    });
    it('should stringify to value', function() {
      a(true);
      expect(JSON.stringify({a: a})).toBe('{"a":true}')
    })
  });
  
  describe('inheritance', function() {
    
    it('should be subclassable', function() {
      function Integer(initial) {
        return Accessor.call(this, initial);
      }
      Integer.prototype = Object.create(Accessor.prototype);
      Integer.prototype.constructor = Integer;
      Integer.prototype.set = function (newVal) {
        newVal = parseInt(newVal, 10) || 0;
        return Accessor.prototype.set.call(this, newVal);
      };
      var i = new Integer('123abc');
      
      expect(i()).toBe(123);
    });
    
  });
});
'use strict';

function Accessor(initial, options) {

  options = options || {};

  var fn = function(newVal) {
    return arguments.length ? fn.set(newVal) : fn.get();
  };

  fn._config = {
    read: !options.hasOwnProperty('read') || options.read,
    write: !options.hasOwnProperty('write') || options.read
  };

  if (options.safe || Object.prototype.__proto__ === undefined) {
    for (var method in Accessor.prototype) {
      if (Accessor.prototype.hasOwnProperty(method)) {
        fn[method] = this[method].bind(fn);
      }
    }
  } else {
    fn.__proto__ = this.__proto__;
  }

  return fn.set(initial, true).update();
}

Accessor.prototype.set = function(newVal, force) {
  if (this._config.write || force) {
    this._current = newVal;
  }
  return this;
};

Accessor.prototype.get = function(force) {
  if (this._config.read || force) {
    return this._current;
  }
};

Accessor.prototype.update = function() {
  this._previous = this._current;
  return this;
};

Accessor.prototype.reset = function() {
  this._current = this._previous;
  return this;
};

Accessor.prototype.changed = function() {
  return this._current !== this._previous;
};

Accessor.prototype.toJSON = function() {
  return this.get(false);
};

// polyfill
if (!Function.prototype.bind) {
  Function.prototype.bind = function(oThis) {
    if (typeof this !== 'function') {
      // closest thing possible to the ECMAScript 5
      // internal IsCallable function
      throw new TypeError('Function.prototype.bind - what is trying to be bound is not callable');
    }

    var aArgs = Array.prototype.slice.call(arguments, 1),
      fToBind = this,
      fNOP = function() {},
      fBound = function() {
        return fToBind.apply(this instanceof fNOP ? this : oThis,
          aArgs.concat(Array.prototype.slice.call(arguments)));
      };

    fNOP.prototype = this.prototype;
    fBound.prototype = new fNOP();

    return fBound;
  };
}