Monday, November 26, 2012

Better HTTP Code Organization in Dart

‹prev | My Chain | next›

I am quite pleased with my very Darty stack right now. In my Dart Comics sample app, I have a client-side MVC library written in Dart, talking to a REST-like web server written in Dart, that stores data in dart dirty a persistent datastore written entirely in Dart. None of this will lay claim to the most sophisticated or cleanest code in the world, but it is pretty cool nonetheless.

Anyhow, before moving on to other topics, I would like to take one more pass at refactoring the HTTP responder code. I have zero intention of building an express.js / Sinatra knockoff—I simply lack the time to begin such an undertaking. Still, I might be able to better than the following:
import 'dart:io';
// ...
main() {
  HttpServer app = new HttpServer();

  app.addRequestHandler(
    (req) {
      if (req.method != 'GET') return false;

      String path = publicPath(req.path);
      if (path == null) return false;

      req.session().data = {'path': path};
      return true;
    },
    (req, res) {
      var file = new File(req.session().data['path']);
      var stream = file.openInputStream();
      stream.pipe(res.outputStream);
    }
  );
  // ...
  app.listen('127.0.0.1', 8000);
}
The problem here is that the intention of that addRequestHandler is buried in the implementation (it serves static files from a public directory). Not only that, but there are a couple of associated functions that are defined elsewhere. Scattered code does not make for maintainable code.

If I had my druthers, I would like to define something like a public class or object that could be supplied to addRequestHandler(). Unfortunately, Dart lacks a "splat" operator or an apply() that could convert an array into a list of arguments. So instead, I settle for something along the lines of:
main() {
  HttpServer app = new HttpServer();

  app.addRequestHandler(Public.matcher, Public.handler);
}
The matcher() and handler() methods will be static (class) methods for the Public class:
class Public {
  static matcher(req) {
    if (req.method != 'GET') return false;

    String path = publicPath(req.path);
    if (path == null) return false;

    req.session().data = {'path': path};
    return true;
  }

  static handler(req, res) {
    var file = new File(req.session().data['path']);
    var stream = file.openInputStream();
    stream.pipe(res.outputStream);
  }

  static String publicPath(String path) {
    if (pathExists("public$path")) return "public$path";
    if (pathExists("public$path/index.html")) return "public$path/index.html";
  }

  static boolean pathExists(String path) => new File(path).existsSync();
}
Aside from using nothing but static methods, which are, of course, tools of the devil, that works perfectly.

For the remaining request handlers in my web server, I will borrow from express.js, which has recently favored the concept of route separation. As in express.js, the matcher functions remain inline with the addRequestHandler() call, but the response handler goes in a separate module.

In other words, this code:
  app.addRequestHandler(
    (req) => req.method == 'GET' && req.path == '/comics',
    (req, res) {
      res.headers.contentType = 'application/json';
      res.outputStream.writeString(JSON.stringify(db.values));
      res.outputStream.close();
    }
  );
Can be rewritten as:
  app.addRequestHandler(
    (req) => req.method == 'GET' && req.path == '/comics',
    Comics.index
  );
And that actually seems to work quite nicely. Once I move the CRUD routes for my comic book database out into the Comics routing class, my entire web server can be expressed as:
main() {
  HttpServer app = new HttpServer();

  app.addRequestHandler(Public.matcher, Public.handler);

  app.addRequestHandler(
    (req) => req.method == 'GET' && req.path == '/comics',
    Comics.index
  );

  app.addRequestHandler(
    (req) => req.method == 'POST' && req.path == '/comics',
    Comics.post
  );

  app.addRequestHandler(
    (req) => req.method == 'DELETE' &&
             new RegExp(r"^/comics/\d").hasMatch(req.path),
    Comics.delete
  );

  app.listen('127.0.0.1', 8000);
}
That may not be express.js / Sinatra level clarity, but it's pretty darn nice. I can even move my DB interaction entirely under that Comics class to further separate and group my concerns. This seems a definite win for my code.


Day #581

1 comment:

  1. Contrary to this post, Dart does have Apply now: https://www.dartlang.org/articles/emulating-functions/#the-apply-method

    ReplyDelete