Oleksiy's Blog

Working with tree objects in JavaScript

04/04/2015

During recent refactoring of the core of my job project, I became really sad when i found these kinds of piece of... code:

storedData.events[requestData.date] = {};
storedData.events[requestData.date][requestData.categoryId] = {};
storedData.events[requestData.date][requestData.categoryId].data = result;
storedData.events[requestData.date][requestData.categoryId].updated = currentTime;
_deferred.resolve(storedData.events[requestData.date][requestData.categoryId].data);

These kinds of blocks were dispersed throughout the application. The structure of nesting this caching tree was different for different cases, so I came up with idea to create a lean and flexible interface for accessing this object.

So by using TDD, I expected to have a method store() which takes dynamic set of arguments, the last of ones will contain the data. E.g.:

cache.store('events', 'today', 16, ['event1', 'event2']);

should store the data and set the timestamp:

{ events: { 
  today: { 
    16: {
      data: ['event1', 'event2'] ,
      timestamp: 1428089159822
    }
  }
} }

I broke down this task to several simpler ones and defined them as functions: pack() - should produces nested tree object of merge() - should recursively merge from source object to target object dive() - should recursively get the value by provided path

As utility I used UnderscoreJS.

Tests first

Alright, so I based on that wrote the tests:

it('should pack to hierarchical structure', function() {
    expect(priv.pack(['events', 'today', 16, ['event1', 'event2']])).
      toEqual({
        events: {
          today: {
            16: {
              data: ['event1', 'event2'],
              updated: mocks.timestamp
            }
          }
        }
      });
  });

  it('should store events data', function() {
    pub.store('events', 'today', 16, ['event1', 'event2']);
    pub.store('events', 'tomorrow', 150, ['event3', 'event4']);
    pub.store('event', 12345, { name: 'Event 5'});

    expect(pub.storedData.events).
      toEqual({
        today: {
          16: {
            data: ['event1', 'event2'],
            updated: mocks.timestamp
          },
        },
        tomorrow: {
          150: {
            data: ['event3', 'event4'],
            updated: mocks.timestamp
          },
        }
      });

    expect(pub.storedData.event).toEqual({
      12345: {
        data: { name: 'Event 5'},
        updated: mocks.timestamp
      }
    });

  });

Nested object from array

function store () {
  var args   = [].slice.apply(arguments), // to array
      source = pack(args);
  merge(storedData, source);
  return source;
}

// creates hierarchical structure and stores last array element as data
function pack (args) {
  var args = args.slice(0),
      obj = {}, arg = args.splice(0,1)[0];

  obj[arg] = args.length > 0 ? pack(args) : undefined;
  return obj[arg] ? obj : new Cached(arg);
}

Deep Merge

function merge (target, source) {
  _.each(_.keys(source), function (key) {
    target[key] = source[key] instanceof Cached ?
      source[key] : target[key] || source[key]; // change if endpoint otherwise choose an existing
    merge(target[key], source[key]);
  });
}

I also created a constructor of cached object to distinguish it in setting and getting:

function Cached (data) {
  _.extend(this, {
    data: data,
    updated: timeFactory.getCurrentTime()
  });
}

For accessing the database i needed a stored() method which recursively travels by object by path given in arguments. This function was slightly simpler to implement.

So I created dive() function and left the business stuff in facade stored() to make it syntactically sweeter.

function stored () {
  var args   = [].slice.apply(arguments),
      cached = dive(args, storedData);
  return cached && cached instanceof Cached ?
    cached.data : undefined;
}

function dive (args, obj) {
  var args = args.slice(0),
      key  = args.shift();
  return !obj ? undefined :
    args.length > 0 ?
      dive(args, obj[key]) : obj[key];
}

Then I added more tests:

  it('should set and get data from/to cache', function() {
    pub.store('events', 'today', 16, ['event1', 'event2']);
    pub.store('events', 'tomorrow', 52, ['event3', 'event4']);

    expect(pub.stored('events', 'today', 16)).
      toEqual(['event1', 'event2']);

    expect(pub.stored('events', 'tomorrow', 52)).
      toEqual(['event3', 'event4']);

    expect(pub.stored('events', 'live', 10)).
      toBeUndefined();

    expect(pub.stored('some', 'invalid', 'path')).
      toBeUndefined();
  });

And this is really helpful thinking in TDD. While hacking my module, I realized some cases that my algorithm didn't do because of silly mistakes. Like this one:

  it('should override existing data', function() {
    pub.store('event', 100, {name: 'old data'});
    pub.store('event', 100, {name: 'new data'});
    pub.store('event', 101, {name: 'other data'});
    expect(pub.stored('event', 100).name).toEqual('new data');
    expect(pub.stored('event', 101).name).toEqual('other data');
  });

In passion of syntactic perfection and optimization, I missed the basic thing: events had not been updating by store() function. Though UT helped me out, as always, I resolved that by adding instance checking merge() function.

Now after adding Cached() constructor it works as just fine.

Although, big refactoring is still in progress...


Password Strength directive for AngularJS

10/28/2014

Have just written yet another tool password strength checker.

Module:

angular.module('myApp.strengthPassword', [])
    .directive('myPasswordStrength', ['passwordStrengthService',
        function (service) {

            function updateValues (password, scope) {
                password            = password || '';
                scope.lengthLevel   = service.getLengthLevel(password);
                scope.strengthLevel = service.getStrengthLevel(password);
                scope.strengthLabel = service.getLabel(scope.strengthLevel);
            }

            function link (scope) {
                scope.$watch('password', function(password) {
                    updateValues(password, scope);
                });
            }

            return {
                restrict: 'EA',
                replace: true,
                scope: { password: '=' },
                templateUrl: 'directives/strengthpassword/strengthpassword.tpl.html',
                link: link
            };
    }])

    .factory('passwordStrengthService', ['passwordStrengthConstants',
        function (constant) {
            var _matchPatterns  = constant.patterns;
            var _maxLengthLevel = constant.maxLengthLevel;

            function _getStrengthLevel (password) {
                for(var level in _matchPatterns) {
                    if (_matchPatterns[level].test(password)) { return level; }
                }
            }

            function _getLengthLevel (password) {
                var level = password.length / _maxLengthLevel * 100;
                return level < 100 ? level : 100;
            }

            function _getLabel (level) {
                return constant.labels[level];
            }

            return {
                getStrengthLevel    : _getStrengthLevel,
                getLengthLevel      : _getLengthLevel,
                getLabel            : _getLabel
            };
        }])

    .constant('passwordStrengthConstants', {
        patterns : {
            empty   : /^$/i,                                                   // not looping if empty
            strong  : /^.*(?=.{8,})(?=.*[a-z])(?=.*[A-Z])(?=.*\d)(?=.*\W).*$/, // 8+ sym, small+capital, digits, alpha
            medium  : /^.*(?=.{6,})(?=.*[a-z])(?=.*[\d\W]).*$/i,               // 6+ sym letters, digits
            weak    : /^.*(?=.{6,})(?=.*[a-z\d]).*$/i,                         // 6+ letters or digits
            useless : /^.*$/i                                                  // anything other
        },

        labels: {
            empty   : '',
            strong  : 'Good password!',
            medium  : 'Password is acceptable, but you could better',
            weak    : 'Your password is a piece of crap',
            useless : 'Password is too short'
        },

        maxLengthLevel : 20
    });

Template:

<div class="password-strength">
    <div class="password-strength-label">
        {{strengthLabel || 'js.passwordStrength'}}
    </div>
    <div class="bar-container">
        <div class="bar {{strengthLevel}}" style="width: {{lengthLevel}}%"></div>
    </div>
</div>

HTML:

<input type="password" name="password" id="password" ng-model="password" />
<div my-password-strength password="password"></div>

LESS/SASS:

.password-strength {
  margin-bottom: 10px;

  .bar-container {
    background: #eee;
    margin-bottom: 10px;

    .password-strength-label {
      margin-bottom: 5px;
    }

    .bar {
      height: 5px;
      -ms-transition:     width .5s ease;
      -webkit-transition: width .5s ease;
      transition:         width .5s ease;

      &.strong  { background: #008641; }
      &.medium  { background: #2573d9; }
      &.weak    { background: #f60; }
      &.useless { background: #e51400; }
    }
  }
}