Saturday, April 21, 2012

Preparing for round-trip conversions between CoffeeScript and Javascript

I have been working quite a bit on a Cordova/PhoneGap plugin Cordova/PhoneGap SQLite plugin (introduced here) and have encountered a need for round-trip conversions between CoffeeScript and JavaScript. The Javascript portion of the iOS version was originally authored in CoffeeScript and compiled to Javascript. For the Android version, I had originally made an adaptation from a version in the Cordova/PhoneGap repository as of October 2011 and then the Javascript portion was largely rebuilt by someone else, based on generated code from the iOS version, to provide some improvements for batch processing. I would like to refactor the code to a common base that can be easily enhanced and maintained in both CoffeeScript and Javascript.


The problem


I already posted several reasons why I find CoffeeScript so much better to work with and I continue to feel the same way for this project, however there are still people in the community who try to stay away from CoffeeScript. Yes there are tools like js2coffee to convert Javascript back to CoffeeScript but there are quite a few problems right now including:

  • By default, the coffee compiler puts the entire code into a special top-level block with the extra indentation but js2coffee does not take the code back out of the block. The only workaround is to add a -b flag to coffee to skip the extra block.
  • Every CoffeeScript class is converted to a block that is achieving the effect in Javascript using constructor functions and prototype members. The conversion back to CoffeeScript will make a small tree of constructor and prototype functions. This becomes worse when class members and/or class functions are used.
  • When var declarations are generated in Javascript, the conversion back to CoffeeScript adds extra statements assigning the variables to undefined.
  • Of course, comments and perhaps extra line spacing are normally ignored. I believe there is a version of js2coffee that can preserve the comment lines but have not taken an opportunity to try it out.

Approach


Of course, the biggest challenge programming-wise is to convert the classes to using constructor and prototype functions. Fortunately I do not have to deal with inheritance for this case so basically each member function is converted to a prototype member declaration, static members get special treatment, and the class declarations are replaced with constructor functions.


The other major change is to enclose the entire CoffeeScript code into a do block, so that only 2 functions will have to be exported.


Changing a member function into a prototype function would indent the function code one block less, while putting the code into a do block would indent the code one block more. So I was able to let the function code remain with some extra indentation, which will be perfect when the code will be put into a do block.


Sample class


So here is a CoffeeScript class, to handle a sqlite transaction:



class SQLitePluginTransaction


  constructor: (@dbPath) ->
    @executes = []


  executeSql: (sql, values, success, error) ->
    txself = @
    successcb = null
    if success
      successcb = (execres) ->
        saveres = execres
        res =
          rows:
            item: (i) ->
              saveres.rows[i]
            length: saveres.rows.length
          rowsAffected: saveres.rowsAffected
          insertId: saveres.insertId || null
        success(txself, res)
    errorcb = null
    if error
      errorcb = (res) ->
        error(txself, res)
    @executes.push getOptions({ query: [sql].concat(values || []), path: @dbPath }, successcb, errorcb)
    return


  complete: (success, error) ->
    throw new Error "Transaction already run" if @__completed
    @__completed = true
    txself = @
    successcb = (res) ->
      success(txself, res)
    errorcb = (res) ->
      error(txself, res)
    begin_opts = getOptions({ query: [ "BEGIN;" ], path: @dbPath })
    commit_opts = getOptions({ query: [ "COMMIT;" ], path: @dbPath }, successcb, errorcb)
    executes = [ begin_opts ].concat(@executes).concat([ commit_opts ])
    opts = { executes: executes }
    Cordova.exec("SQLitePlugin.backgroundExecuteSqlBatch", opts)
    @executes = []
    return

and the generated Javascript (with no extra block indentation, small font to save space):




SQLitePluginTransaction = (function() {


  function SQLitePluginTransaction(dbPath) {
    this.dbPath = dbPath;
    this.executes = [];
  }


  SQLitePluginTransaction.prototype.executeSql = function(sql, values, success, error) {
    var errorcb, successcb, txself;
    txself = this;
    successcb = null;
    if (success) {
      successcb = function(execres) {
        var res, saveres;
        saveres = execres;
        res = {
          rows: {
            item: function(i) {
              return saveres.rows[i];
            },
            length: saveres.rows.length
          },
          rowsAffected: saveres.rowsAffected,
          insertId: saveres.insertId || null
        };
        return success(txself, res);
      };
    }
    errorcb = null;
    if (error) {
      errorcb = function(res) {
        return error(txself, res);
      };
    }
    this.executes.push(getOptions({
      query: [sql].concat(values || []),
      path: this.dbPath
    }, successcb, errorcb));
  };


  SQLitePluginTransaction.prototype.complete = function(success, error) {
    var begin_opts, commit_opts, errorcb, executes, opts, successcb, txself;
    if (this.__completed) throw new Error("Transaction already run");
    this.__completed = true;
    txself = this;
    successcb = function(res) {
      return success(txself, res);
    };
    errorcb = function(res) {
      return error(txself, res);
    };
    begin_opts = getOptions({
      query: ["BEGIN;"],
      path: this.dbPath
    });
    commit_opts = getOptions({
      query: ["COMMIT;"],
      path: this.dbPath
    }, successcb, errorcb);
    executes = [begin_opts].concat(this.executes).concat([commit_opts]);
    opts = {
      executes: executes
    };
    Cordova.exec("SQLitePlugin.backgroundExecuteSqlBatch", opts);
    this.executes = [];
  };

  return SQLitePluginTransaction;

})();

and when converted back to CoffeeScript:

SQLitePluginTransaction = (->
  SQLitePluginTransaction = (dbPath) ->
    @dbPath = dbPath
    @executes = []
  SQLitePluginTransaction::executeSql = (sql, values, success, error) ->
    errorcb = undefined
    successcb = undefined
    txself = undefined
    txself = this
    successcb = null
    if success
      successcb = (execres) ->
        res = undefined
        saveres = undefined
        saveres = execres
        res =
          rows:
            item: (i) ->
              saveres.rows[i]

            length: saveres.rows.length

          rowsAffected: saveres.rowsAffected
          insertId: saveres.insertId or null

        success txself, res
    errorcb = null
    if error
      errorcb = (res) ->
        error txself, res
    @executes.push getOptions(
      query: [ sql ].concat(values or [])
      path: @dbPath
    , successcb, errorcb)

  SQLitePluginTransaction::complete = (success, error) ->
    begin_opts = undefined
    commit_opts = undefined
    errorcb = undefined
    executes = undefined
    opts = undefined
    successcb = undefined
    txself = undefined
    throw new Error("Transaction already run")  if @__completed
    @__completed = true
    txself = this
    successcb = (res) ->
      success txself, res

    errorcb = (res) ->
      error txself, res

    begin_opts = getOptions(
      query: [ "BEGIN;" ]
      path: @dbPath
    )
    commit_opts = getOptions(
      query: [ "COMMIT;" ]
      path: @dbPath
    , successcb, errorcb)
    executes = [ begin_opts ].concat(@executes).concat([ commit_opts ])
    opts = executes: executes
    Cordova.exec "SQLitePlugin.backgroundExecuteSqlBatch", opts
    @executes = []

  SQLitePluginTransaction
)()

This code is not so great to work with, considering the extra block with indentation that is added in the round trip from a class declaration.

Prototype members

So the first task is to convert the member functions to prototype members, the diffsS are shown here:

@@ -111,7 +112,7 @@ class SQLitePluginTransaction
   constructor: (@dbPath) ->
     @executes = []
 
-  executeSql: (sql, values, success, error) ->
+SQLitePluginTransaction::executeSql = (sql, values, success, error) ->
     txself = @
     successcb = null
     if success
@@ -132,7 +133,7 @@ class SQLitePluginTransaction
     @executes.push getOptions({ query: [sql].concat(values || []), path: @dbPath }, successcb, errorcb)
     return
 
-  complete: (success, error) ->
+SQLitePluginTransaction::complete = (success, error) ->
     throw new Error "Transaction already run" if @__completed
     @__completed = true
     txself = @

So all that remains in the original class block is the constructor function. The generated Javascript is omitted in order to save some space. Here is the CoffeeScript that comes out of the round trip:

SQLitePluginTransaction = (->
  SQLitePluginTransaction = (dbPath) ->
    @dbPath = dbPath
    @executes = []
  SQLitePluginTransaction
)()
SQLitePluginTransaction::executeSql = (sql, values, success, error) ->
  errorcb = undefined
  successcb = undefined
  txself = undefined
  txself = this
  successcb = null
  if success
    successcb = (execres) ->
      res = undefined
      saveres = undefined
      saveres = execres
      res =
        rows:
          item: (i) ->
            saveres.rows[i]

          length: saveres.rows.length

        rowsAffected: saveres.rowsAffected
        insertId: saveres.insertId or null

      success txself, res
  errorcb = null
  if error
    errorcb = (res) ->
      error txself, res
  @executes.push getOptions(
    query: [ sql ].concat(values or [])
    path: @dbPath
  , successcb, errorcb)

SQLitePluginTransaction::complete = (success, error) ->
  begin_opts = undefined
  commit_opts = undefined
  errorcb = undefined
  executes = undefined
  opts = undefined
  successcb = undefined
  txself = undefined
  throw new Error("Transaction already run")  if @__completed
  @__completed = true
  txself = this
  successcb = (res) ->
    success txself, res

  errorcb = (res) ->
    error txself, res

  begin_opts = getOptions(
    query: [ "BEGIN;" ]
    path: @dbPath
  )
  commit_opts = getOptions(
    query: [ "COMMIT;" ]
    path: @dbPath
  , successcb, errorcb)
  executes = [ begin_opts ].concat(@executes).concat([ commit_opts ])
  opts = executes: executes
  Cordova.exec "SQLitePlugin.backgroundExecuteSqlBatch", opts
  @executes = []

So the member functions are much better to work with after going through the round trip and now the issue remains with the constructor.

Constructor function

So all that remains of the class declaration:ss

class SQLitePluginTransaction


  constructor: (@dbPath) ->
    @executes = []

So to replace this with the constructor function, we can extract the constructor function from the generated code above leaving out the extra declaration block:
  SQLitePluginTransaction = (dbPath) ->
    @dbPath = dbPath
    @executes = []

But there is something that js2coffee does not tell us: we need to add an empty return statement otherwise the constructor function will return the last expression evaluated and the user application code will fail. So here is the desired function constructor code:

SQLitePluginTransaction = (dbPath) ->
    @dbPath = dbPath
    @executes = []
    return

and after a round trip:

SQLitePluginTransaction = (dbPath) ->
  @dbPath = dbPath
  @executes = []

SQLitePluginTransaction::executeSql = (sql, values, success, error) ->
  errorcb = undefined
  successcb = undefined
  txself = undefined
  txself = this
  successcb = null
  if success
    successcb = (execres) ->
      res = undefined
      saveres = undefined
      saveres = execres
      res =
        rows:
          item: (i) ->
            saveres.rows[i]

          length: saveres.rows.length

        rowsAffected: saveres.rowsAffected
        insertId: saveres.insertId or null

      success txself, res
  errorcb = null
  if error
    errorcb = (res) ->
      error txself, res
  @executes.push getOptions(
    query: [ sql ].concat(values or [])
    path: @dbPath
  , successcb, errorcb)

SQLitePluginTransaction::complete = (success, error) ->
  begin_opts = undefined
  commit_opts = undefined
  errorcb = undefined
  executes = undefined
  opts = undefined
  successcb = undefined
  txself = undefined
  throw new Error("Transaction already run")  if @__completed
  @__completed = true
  txself = this
  successcb = (res) ->
    success txself, res

  errorcb = (res) ->
    error txself, res

  begin_opts = getOptions(
    query: [ "BEGIN;" ]
    path: @dbPath
  )
  commit_opts = getOptions(
    query: [ "COMMIT;" ]
    path: @dbPath
  , successcb, errorcb)
  executes = [ begin_opts ].concat(@executes).concat([ commit_opts ])
  opts = executes: executes
  Cordova.exec "SQLitePlugin.backgroundExecuteSqlBatch", opts
  @executes = []

Much better, hey? The only major problem I see is that the empty return statements were forgotten by js2coffee. Yeah some extra undefined declarations can also be removed. But at least this is usable, in case I receive patches to the Javascript version.

Class members and methods

I have actually just learned something about how class members work in CoffeeScript, which will lose their meaning in the translation to Javascript. So for one of the classes, I had first factored the regular member functions into prototype members and the following remains:

class SQLitePlugin

  constructor: (@dbPath, @openSuccess, @openError) ->
    throw new Error "Cannot create a SQLitePlugin instance without a dbPath" unless dbPath
    @openSuccess ||= () ->
      console.log "DB opened: #{dbPath}"
      return
    @openError ||= (e) ->
      console.log e.message
      return
    @open(@openSuccess, @openError)

  # Note: Class member
  # All instances will interact directly on the prototype openDBs object.
  # One instance that closes a db path will remove it from any other instance's perspective as well.
  openDBs: {}

  # Note: Class method
  @handleCallback: (ref, type, obj) ->
    callbacks[ref]?[type]?(obj)
    callbacks[ref] = null
    delete callbacks[ref]
    return

Which becomes after a round-trip:

SQLitePlugin = (->
  SQLitePlugin = (dbPath, openSuccess, openError) ->
    @dbPath = dbPath
    @openSuccess = openSuccess
    @openError = openError
    throw new Error("Cannot create a SQLitePlugin instance without a dbPath")  unless dbPath
    @openSuccess or (@openSuccess = ->
      console.log "DB opened: " + dbPath
    )
    @openError or (@openError = (e) ->
      console.log e.message
    )
    @open @openSuccess, @openError
  SQLitePlugin::openDBs = {}
  SQLitePlugin.handleCallback = (ref, type, obj) ->
    _ref = undefined
    _ref[type] obj  if typeof _ref[type] is "function"  if (_ref = callbacks[ref])?
    callbacks[ref] = null
    delete callbacks[ref]

  SQLitePlugin
)()

and the hints show up again: class member like SQLitePlugin::openDBs = ... and class method like SQLitePlugin.handleCallback = ...

So to fix the class member and method:
@@ -62,13 +61,13 @@ class SQLitePlugin
       return
     @open(@openSuccess, @openError)
 
-  # Note: Class member
-  # All instances will interact directly on the prototype openDBs object.
-  # One instance that closes a db path will remove it from any other instance's perspective as well.
-  openDBs: {}
+# Note: Class member
+# All instances will interact directly on the prototype openDBs object.
+# One instance that closes a db path will remove it from any other instance's perspective as well.
+SQLitePlugin::openDBs = {}
 
-  # Note: Class method
-  @handleCallback: (ref, type, obj) ->
+# Note: Class method (will be exported by a member of root.sqlitePlugin)
+SQLitePlugin.handleCallback = (ref, type, obj) ->
     callbacks[ref]?[type]?(obj)
     callbacks[ref] = null
     delete callbacks[ref]

So to fix the constructor:

@@ -50,8 +50,11 @@ getOptions = (opts, success, error) ->
     opts.callback = cbref(cb) if has_cbs
     opts
 
-class SQLitePlugin
-  constructor: (@dbPath, @openSuccess, @openError) ->
+# Prototype constructor function
+SQLitePlugin = (dbPath, openSuccess, openError) ->
+    @dbPath = dbPath
+    @openSuccess = openSuccess
+    @openError = openError
     throw new Error "Cannot create a SQLitePlugin instance without a dbPath" unless dbPath
     @openSuccess ||= () ->
       console.log "DB opened: #{dbPath}"
@@ -60,6 +63,7 @@ class SQLitePlugin
       console.log e.message
       return
     @open(@openSuccess, @openError)
+    return

Further

The one thing I have not really gone through here is to put the contents into a do -> block and then use the -b flag to tell the coffee compiler not to add an extra block. Then the results of round-trip transformations between CoffeeScript and Javascript should start to become less painful.

Wednesday, April 18, 2012

Cordova/PhoneGap iOS Plugin Return Result

Someone raised an excellent question on how to get the return result from a Cordova/PhoneGap iOS plugin. The articles from phonegap.com and from adobe.com do give an answer but you have to look very carefully and there is a much easier way which I will document here for the iOS and in another post for the Android.

Basically, a Cordova/PhoneGap plugin returns a result asynchronously by sending some Javascript code to be executed in the app's browser. Here is an example from the DatePicker plugin, with small adaptions to make it easier to read (extra spacing, using single quote in Javascript string):

- (void)actionSheet:(UIActionSheet *)actionSheet willDismissWithButtonIndex:(NSInteger)buttonIndex
{
    NSString* jsCallback = [NSString stringWithFormat:
        @"window.plugins.datePicker._dateSelected('%i');",
        (int)[self.datePicker.date timeIntervalSince1970]];
    [super writeJavascript:jsCallback];
}


So this code actually wants to execute a piece of code on the Javascript side to report a result that a date had been selected. Here is the code on the Javascript side that will be called back with the date that has been selected:

    DatePicker.prototype._dateSelected = function(date) {
        var d = new Date(parseFloat(date)*1000);
        if (this._callback)
            this._callback(d);
    }


Assuming you remember how prototype works you can see that this function will parse the value that is returned and make a callback to the Javascript application code, if it exists.


Update to answer a comment: the _callback member is set according to a parameter from the original function call, for example:
    DatePicker.prototype.show = function(options, cb) {
        // [options code omitted, TBD describe in another post]
        this._callback = cb;
        Cordova.exec("DatePicker.show", options);
    } 

The Cordova/PhoneGap documents describe how to use a CDVPluginResult object to return a plugin result object with data that will be sent to either a success callback or an error callback that was passed into cordova.exec() function but I find the method described here much easier to understand and debug.

Tuesday, April 17, 2012

When to use CoffeeScript?

Plenty of us are writing applications in Javascript, whether we are writing mobile app code, getting the most of node.js, or just building a website. The CoffeeScript invention came out a few years ago and it really looks great with its promise to make app programming easier.

But we still have to learn Javascript, or at least the good parts, including some things like its functional programming syntax which can get hard to read and oh yeah you have to use === instead of == in a number of cases. CoffeeScript makes these things much easier but if we have to start debugging or using other people's libraries we still have to deal with some of these quirks in Javascript.

And for some rapid development tasks, we can question if it is even worth adding the extra compile step to start using CoffeeScript.

I believe it is worth adding the extra steps to start using CoffeeScript if there are multiple modules or any form of OOP or functional programming are needed. I spent 1-2 months building a mobile application for a client using Javascript modules until I started working on a Cordova/PhoneGap plugin written in CoffeeScript. I started making some changes to its API and really found the CoffeeScript so much easier to manage and refactor. I was able to use js2coffee to convert most of my modules to CoffeeScript and it is so much easier to make the changes and refactor into smaller modules.

This morning I needed a web server that calls a JSON API and returns a certain part of the results. In node.js, there are a couple of levels of callbacks that are needed to handle the asynchronous nature on all sides. I thought about doing it in Javascript and realized that the complexity of handling the callbacks and functional programming can really blow up and it will be much smoother in CoffeeScript. I wrote about how to do the JSON services in node.js here.

The only time I see not to use CoffeeScript is in a community where other people are still using Javascript. I have worked on a Cordova/PhoneGap SQL API and other people have been helping to make some improvements in the Javascript version. In this case, I still want to keep the CoffeeScript version to make refactoring easier but will have to make the Javascript version leading.

Friday, April 13, 2012

Notes on Running Cordova/PhoneGap on Android without Eclipse

NOTE: this information for developing on both OSX & Linux is now given in this entry in my new blog.

Running an old PhoneGap iPhone plugin on Cordova 1.5+ iOS

I have been thinking about the nightmare of supporting multiple versions of Cordova/PhoneGap and had an idea to make a shim to build and run old PhoneGap plugins on Cordova iOS 1.5+. Of course we want the plugins to migrate to the new Cordova API and abandon the old one but this will not happen overnight.

So far I tried making an Objective-C PGPlugin class, deriving from CDVPlugin in PGPlugin.[hm] so that an old PhoneGap plugin can compile and run. Of course we will need some more definitions to support certain plugins but this was a nice quick and easy test. For a first test I discovered phonegap-plugins / iPhone / Prompt which had not yet been ported to Cordova 1.5+.

I put the PGPlugin.[hm] in the Plugins folder, adding a Javascript file, using the test code in onDeviceReady(), and it worked the first time!

I discovered that the extra Javascript to define window.PhoneGap is not necessary before Cordova 2.0, but I checked it in anyway to be prepared for the future.

So I have a start to run old PhoneGap plugins on Cordova 1.5+. Next is to try some more old PhoneGap plugins, especially ones that are exhibiting more dependencies.

UPDATE: test with the BundleFileReader is working great with a typedef and 2 enum values added to PGPlugin.h.

My first Cordova 1.6.0 iOS project: generated by command line

UPDATE: this is no longer relevant, please see the PhoneGap command-line guide for iOS for instructions.

I now have the pleasure to restart my Cordova project based on Cordova 1.6.0. I tried uninstalling Cordova 1.5.0 and then installing Cordova 1.6.0 but my Xcode 4.3.2 decided to use the old template to construct my Cordova project. I got some very nice messages of  built-in "plugins" not being found so I decided to try making my project from the command line.

I found Shazron had already posted  how to create a PhoneGap project from the command line so I made an adaptation (UPDATE: no longer valid) to create a Cordova 1.5/1.6+ project. I am actually very happy with the results since you don't have to drag-and-drop the www folder. You have to make sure you select the right project and target to run the generated project. UPDATE: there will be some ugly log messages in the debug mode so it is better to go into release mode (Apache issue CB-502 has been filed).

Cordova iOS 1.6.0 changed the global Cordova to cordova to be consistent with the Android version, so something like << Cordova ||= cordova; >> has to be added to the Plugin Javascript to work for both Cordova 1.5 and 1.6.

I tried including a couple plugins including an adaptation of the Globalization plugin and chbrody / Cordova-SQLitePlugin and they seemed to be working fine. UPDATE this is no longer valid: Some changes have been checked into https://github.com/chbrody/Cordova-SQLitePlugin/blob/master/Cordova-iOS/Plugins/SQLitePlugin.h, probably due to the different structure generated by create-ios-project.sh.

The next challenge is to run the program on a device. Since the command line did not provide an option to specify the company identifier, simply fix the Bundle identifier on the project summary pane to be like <<MYIDPREFIX.AppName>> and it should work with a wildcard app ID.

Now if you do not want to use the wildcard app identifier you have to go back to the Bundle and take out your ID prefix so the Bundle identifier should be like <<AppName>>. You can also remove your wildcard provisioning profile from the Xcode organizer to make sure it is matching the proper, desired provisioning profile for your app. So the Xcode has stored your ID prefix somewhere, I don't know where but this actually is working for me. If anyone knows how it works please post a comment with a link to a good explanation.