Friday, August 22, 2014

QUnit tests on AngularJS directives

Recently, I had an little homework assignment to create a "signup" web application using Angular.js. You can see the app here.

As part of that assignment, I created an Angular directive to validate the data in the "password" and "verification" text inputs.  Here's what the Angular directive looked like:

// match password and verification
app.directive('match', [function () {
  return {
    require: 'ngModel',
    link: function(scope, elem, attrs, ctrl) {
      scope.$watch('['+attrs.ngModel+', '+attrs.match+']',
          function(value){
        ctrl.$setValidity('match', value[0] === value[1]);
      }, true);
    }
  };
}]);

But I wanted to use QUnit to test this directive.  I spent a ton of time, trying a zillion things, which didn't work because I was a newbie to both Angular and QUnit (and using them together).  But, finally, I found the correct combination.

// create test bed
var injector = angular.injector(['ng', 'ngMock', 'signupApp']);

var init = {
  setup: function() {
    this.$scope = injector.get('$rootScope').$new();
  }
};

module('tests', init);

// test the 'match' directive
QUnit.test('match', function() {
  var html = '<form id="myform" name="signupController" ng-controller="signupController"><input id="password" ng-model="password" match="verification"></input><input id="verification" ng-model="verification" match="password"></input></form>';
  var $compile = injector.get('$compile');
  var element = $compile(html)(this.$scope);
  this.$scope.password = 'passw0rd';
  this.$scope.verification = 'passw0rd';
  this.$scope.$apply();
  ok(element.scope().signupController.$valid, '$valid is false');
  this.$scope.password = 'passw0rd';
  this.$scope.verification = 'passw0rd2';
  this.$scope.$apply();
  ok(!element.scope().signupController.$valid, '$valid is true when it should be false');
});

My first mistake was that it took me a long time to figure out that I needed angular-mocks.js to create a fully functional test bed.  The ngMock module is needed to add lots of support to the Angular test harness.  While I could get a simple test harness running without angular-mocks.js, testing that required the controller (and probably ngModel) required angular-mocks.js.

My second mistake was that it took me a long time to realize that the Angular scope and the Angular controller are different.  (Well, duh.)  Finally, I discovered that, if I added a "name" attribute to the controller element, a property giving access to the controller would be available in the scope.

This may be obvious to Angular experts but I spent a ton of time traveling around on Google and I never found any posts that were on point.

Thursday, July 3, 2014

Events Are More Flexible Than HTTP

When you first use $.ajax() in jQuery, it is easy and tempting to set up a HTTP request/response style communication mechanism between your client and your server.  Your client sends a HTTP request, it provides a handler to receive the HTTP response and the handler does something with the result.  That seems fine ...

... except it breaks down pretty easily.  If the server takes too long, the HTTP connection times out and the response is lost.  And, when it breaks down, you hack on it, adding polling and/or subscription mechanisms.  Then it breaks down some more and you hack on it some more.

Events are always better.

With events, the client sends and receives events (e.g. a JSON object).

Events are self-contained.  They do not require any external information.  For example, the event should not have a different meaning if it is called on one URI (e.g. /users) versus another URI (e.g. /groups).  If it does, the event should be updated with that new information to keep the event self-contained.  So, in our example, maybe an "domain" key is added to the event which is assigned a value of "users" or "groups".  After modifying the event, the event would again be self-contained and the URI that it was sent to can be forgotten.

An event should also be disconnected from the communications mechanism: it should not matter if an event was received via a HTTP request (or HTTP response), short-polling, long-polling, JSONP, Socket.IO or even some strange new datagram mechanism (which would be session-less and not allow responses).  Different communication transports should be easy to substitute.

Events work in "fire and forget" mode.  Once an event is fired, it is gone.  The sender does not concern itself whether the event is received or not; it lets the event delivery mechanism do its work and deliver the event with no further interaction.

But what if your code expects a response?

The event handler should receive the event, process it and then send a response event back to the original sender.  A response event is new event that is created by the receiver and sent to the original sender with a reference to the original event.  Usually, the events contain a unique event number which the response event can reference.  The response is still a new event; it just references another event.

With HTTP, an assumption is made that every request has its own response.  This assumption may not be valid when you are using long-polling instead of HTTP.  With events, however, the assumption is removed; events can be sent and response events can be sent back using some other mechanism or at a much later date.  HTTP relies on the response being sent back in the same HTTP connection as the HTTP request was sent.  Events do not.

Events can be built on top of HTTP requests/responses.  Events are sent as part of the GET query string or the POST data and any pending events can be returned in the HTTP response body.  The difference is that the events returned do not need to correspond to the events sent.  Arbitrary events can be sent and arbitrary events can be received in the same HTTP connection with no relationship implied between them.

Inevitably, it seems that most systems move towards an event system (or suffer through an ever growing number of hacks to add flexibility to HTTP request/response designs).

If you want my advice, consider starting any new code with events, rather than muddling through with an HTTP request/response system and converting it later.

Friday, January 3, 2014

Super simple JSON and MySQL

I invented jsonhib (available for Node.js and PHP at http://github.com/ajaximrpg/jsonhib ) to provide a "good enough" solution to reading and writing JSON data to a MySQL database.

Most developers say this is impossible.

But, to read JSON from a MySQL database, jsonhib has a readRows() method that takes two arguments: a table name and a WHERE clause.  It returns a JSON array of JSON objects where each object represents a MySQL row.  The columns of each MySQL row become JSON property names; the values of each MySQL row become property values.  The WHERE clause only serves to narrow the number of JSON objects returned.

In Node.js, it looks like this:

// assume 'mytable' is a MySQL table with these columns: id, name
jh.readRows('mytable', 'WHERE id > 0', function(s) {
  var str = s;
});
// str='[{"id": 1, "name": "bob"}, {"id": 2, "name": "fred"}]'

Most developers will object that the objects are out of order.  "There's no sorting or ordering," they say, "the objects come out in random order."

They sure do.  Sometimes, the client doesn't care so it doesn't matter.  But, if it does matter and an additional integer column can be added to the table (a sort_column column), jsonhib can be directed to use this hidden column to maintain the order of rows.  And, if the table can't be modified, jsonhib relies on the caller to reorder the JSON array if he wishes.

Another objection is that MySQL databases require a schema and JSON objects can have arbitrary properties.  "The only choice is to put all your JSON objects in one table and serialize the JSON to a single MySQL text column," they say.

Uh, is that how you do it in client?  Just have all your JSON objects crammed into a single humongous array?  Mix your customer JSON objects with your sales order JSON objects and your permissions JSON objects?  Of course not!  You assign arrays of similar JSON objects to different variables.  In this case, similar JSON objects are assigned to specific MySQL tables.  Your client doesn't add customer JSON objects to the permissions variable; don't add customer data to the MySQL permissions table.

Also, don't all customers have a name?  Isn't their name always a string, not a floating point value?  Yes, JSON objects can have arbitrary properties but they always have a lot of properties that are expected and required and are of a specific type.  jsonhib relies on the caller to know that, by default, JSON properties that do not fit into the MySQL schema will be discarded.  Avoid extra properties or store them somewhere else.  If that is not desirable and an additional string column can be added to the table (a json_column column), jsonhib can be directed to use this hidden column to keep track of JSON data that doesn't fit into the MySQL schema.  This column doesn't keep all the JSON data; it just keeps the JSON properties that don't have a corresponding dedicated MySQL column or have a MySQL column of the wrong type.

jsonhib really is just an object-relational mapping (ORM) layer for JSON and MySQL.  When reading, jsonhib queries MySQL rows and sensibly maps MySQL columns to JSON properties and, if available, uses the sort_column and json_column columns to fix up the mismatches between how MySQL works and how JSON works.

Besides reading, jsonhib can also insert, delete and update JSON objects.  JSON arrays (i.e. MySQL tables) can also be reordered using the moveRow() method.

// insert a row
jh.insertRow('mytable', '', -1, '{"id": 1, "name": "bob"}',
  function() {});
// delete a row
jh.deleteRow('mytable', '', 0,
  function() {});
// update a row
jh.updateRow('mytable', 'WHERE id=1', 0, '{"id": 1, "name": "eric"}',
  function() {});
// move a row (huh?)
jh.moveRow('mytable', 'WHERE id=1', 0, 1,
  function() {});

jsonhib has a lot of nice attributes.  Other solutions require new SQL syntax provided by specially modified MySQL database software, plugins or new versions but jsonhib works with any MySQL version and any MySQL data.  Existing applications and processes that use the MySQL database work without modification and can work with data that is inserted, updated and deleted using jsonhib.

Impossible?  No, not impossible.  JSON can be stored in MySQL.


Rewrite a CSS stylesheet using PHP

I've been writing a user-generated content system in PHP where users can upload CSS (and HTML) that will be shown in a DIV.  Of course, I wanted to restrict their CSS to only be applied inside the DIV and not style the surrounding content (either accidentally or maliciously).

The first step was easy: create a class and assign it to the DIV.

<div class="user_content">

But how to restrict a CSS stylesheet with a bunch of random rules to that DIV?

Given something like this:

.block input, table td.money {
  ...
}

How do I turn it into this?

.user_content .block input, .user_content table td.money {
  ...
}

A long search finally ended when Stack Overflow revealed a way to insert my user_content class before every rule using a fancy regular expression ... but it broke down on comments with commas in them.  It was a long search because it was very difficult to figure out what search terms to use and a real bummer that the solution broke down on something as common as comments.

So, I developed this code to handle block out the commas in comments, apply the fancy regular expression from Stack Overflow and then put the commas back.

function rewriteCss($css, $cssClazz) {
  while (preg_match('/(\/\*.+?),(.+?\*\/)/', $css) === 1) {
    $contents = preg_replace(
      '/(\/\*.+?),(.+?\*\/)/', '$1:comma:$2', $css);
  }
  $css = preg_replace(
    '/([^\r\n,{}]+)(,\s*(?=[^}]*{)|\s*{)/',
    '.'.$cssClazz.' $1$2', $css);
  while (preg_match('/(\/\*.+?):comma:(.+?\*\/)/', $css) === 1) {
    $css = preg_replace(
      '/(\/\*.+?):comma:(.+?\*\/)/', '$1,$2', $css);
  }
  return $css;
}

That did the trick.  Each comma in a comment is blocked out by replacing it with :comma:.  Then, a complicated preg_replace() call is made with the fancy regular expression from Stack Overflow.  Finally, the :comma: in comments are restored to being literal commas.

Super.