veyron-browser: Plugin system for RPC output

Originally, JSON.stringify printed out all values regardless of type.
However, RPC output sometimes should be rendered differently.

In the case of monitoring, we receive an object, a histogram.
The histogram, when detected, will be rendered specially.
This is the motivation for a new plugin system for rendering RPC output

The new plugin system consists of 2 parts
* Given a value, identify the correct plugin to use
* Use the plugin to render the value

The code has been restructured to accommodate plugins for rendering output.

Change-Id: I910303d33582b563544d237c9027a539853f86bf
diff --git a/src/components/browse/item-details/index.css b/src/components/browse/item-details/index.css
index ecf28c1..fd575e7 100644
--- a/src/components/browse/item-details/index.css
+++ b/src/components/browse/item-details/index.css
@@ -76,6 +76,10 @@
   margin: 0.5em;
 }
 
+.method-output pre, .field pre {
+  white-space: pre-wrap;
+}
+
 .background {
   width: 200px;
   height: 30px;
diff --git a/src/components/browse/item-details/index.js b/src/components/browse/item-details/index.js
index e403137..e8b9da0 100644
--- a/src/components/browse/item-details/index.js
+++ b/src/components/browse/item-details/index.js
@@ -41,8 +41,8 @@
     /*
      * The details information for each service object.
      * Can include recommended details information.
-     * @type {map[string]map[string]string|float}
-     * details information: string
+     * @type {map[string]map[string]string|mercury|float}
+     * details information: string or mercury element
      * recommended details information: float
      */
     details: mercury.varhash(),
@@ -135,8 +135,9 @@
     if (details.hasOwnProperty(method)) {
       // TODO(alexfandrianto): We may wish to replace this with something less
       // arbitrary. Currently, strings are treated as stringified RPC output.
-      // Floats are treated as the prediction values of recommended items.
-      if (typeof details[method] === 'string') {
+      // And mercury elements can also be rendered this way.
+      // Numbers are treated as the prediction values of recommended items.
+      if (typeof details[method] !== 'number') {
         // These details are already known.
         displayItems.push(
           renderFieldItem(
@@ -290,7 +291,9 @@
 
     AnimationHook.prototype.hook = function (elem, propName) {
       // On animation end, call the method.
-      function animationEndHandler() {
+      function animationEndHandler(e) {
+        // TODO(alexfandrianto): I think mercury may be the one at fault, but...
+        // this handler is sometimes called several times on animation end.
         events.methodCalled({
           name: state.itemName,
           methodName: methodName,
diff --git a/src/components/browse/item-details/make-rpc.js b/src/components/browse/item-details/make-rpc.js
index ab6f71a..1cace44 100644
--- a/src/components/browse/item-details/make-rpc.js
+++ b/src/components/browse/item-details/make-rpc.js
@@ -1,31 +1,40 @@
 var browseService = require('../../../services/browse-service');
 var smartService = require('../../../services/smart-service');
 var debug = require('debug')('make-rpc');
+var renderDetail = require('./render-detail');
 
 module.exports = makeRPC;
 
 /*
  * Use the browseService to perform an RPC request.
  * Put the results in the state and record this request in the smartService.
+ * Note that the recorded results are rendered according to renderDetail.
  * data needs to have (name, methodName, args, hasParams, signature)
  */
 function makeRPC(state, data) {
   browseService.makeRPC(data.name, data.methodName, data.args).then(
     function(result) {
       debug('Received:', result);
-      if (result.toString().length > 0) {
-        state.methodOutputs.push(JSON.stringify(result));
+
+      // Do not process empty results.
+      // TODO(alexfandrianto): Eventually, we will know from the method
+      // signature if there are actually results we should care about.
+      if (result.toString().length === 0) {
+        return;
       }
 
+      // Use renderDetail to process the raw result into a renderable format.
+      var renderedResult = renderDetail(result);
+      state.methodOutputs.push(renderedResult);
+
       // If we received a result for a 0-parameter RPC, add to the details page.
-      // TODO(alexfandrianto): Remove the debug lines in this block.
-      if (!data.hasParams && result.toString().length > 0) {
+      if (!data.hasParams) {
         // Store the data we received in our state for later rendering.
         var detail = state.details.get(data.name);
         if (detail === undefined) {
           detail = {};
         }
-        detail[data.methodName] = JSON.stringify(result); // convert to string
+        detail[data.methodName] = renderedResult;
         state.details.put(data.name, detail);
 
         // Log the successful RPC to the smart service.
@@ -35,7 +44,6 @@
           name: data.name,
           reward: 1
         };
-
         smartService.record('learner-autorpc', input);
 
         // For debug, display what our prediction would be.
diff --git a/src/components/browse/item-details/plugins/default.js b/src/components/browse/item-details/plugins/default.js
new file mode 100644
index 0000000..68d16e9
--- /dev/null
+++ b/src/components/browse/item-details/plugins/default.js
@@ -0,0 +1,20 @@
+var h = require('mercury').h;
+
+module.exports = {
+  'shouldRender': shouldRender,
+  'render': render
+};
+
+/*
+ * By default, always render.
+ */
+function shouldRender(input) {
+  return true;
+}
+
+/*
+ * By default, the input is returned as prettified JSON.
+ */
+function render(input) {
+  return h('pre', JSON.stringify(input, null, 2));
+}
\ No newline at end of file
diff --git a/src/components/browse/item-details/plugins/histogram.js b/src/components/browse/item-details/plugins/histogram.js
new file mode 100644
index 0000000..85699ce
--- /dev/null
+++ b/src/components/browse/item-details/plugins/histogram.js
@@ -0,0 +1,28 @@
+var histogram = require('bars');
+var h = require('mercury').h;
+
+module.exports = {
+  'shouldRender': shouldRender,
+  'render': render
+};
+
+/*
+ * Render if the appropriate histogram fields are present.
+ * TODO(alexfandrianto): Negotiate a better way of identifying histogram data.
+ */
+function shouldRender(input) {
+  return input.count !== undefined && input.sum !== undefined &&
+    input.buckets !== undefined;
+}
+
+/*
+ * The histogram is rendered with bars (a fork of ascii-histogram).
+ * TODO(alexfandrianto): Consider using a prettier rendering package.
+ */
+function render(input) {
+  var histData = {};
+  input.buckets.forEach(function(obj) {
+    histData[obj.lowBound] = obj.count;
+  });
+  return h('pre', histogram(histData, { bar: '*', width: 20 }));
+}
\ No newline at end of file
diff --git a/src/components/browse/item-details/render-detail.js b/src/components/browse/item-details/render-detail.js
new file mode 100644
index 0000000..9912724
--- /dev/null
+++ b/src/components/browse/item-details/render-detail.js
@@ -0,0 +1,25 @@
+/*
+ * The plugins listed here are listed in order with highest priority first.
+ * Plugins must export functions shouldRender(input) and render(input).
+ * The default plugin should always be last.
+ */
+var plugins = [
+  require('./plugins/histogram.js'),
+  require('./plugins/default.js')
+];
+
+module.exports = renderDetail;
+
+/*
+ * Transforms the input into the desired detail output.
+ * Various plugins are tested until the correct one is found.
+ * With the default plugin, this should always return something.
+ */
+function renderDetail(input) {
+  for (var i = 0; i < plugins.length; i++) {
+    if (plugins[i].shouldRender(input)) {
+      return plugins[i].render(input);
+    }
+  }
+  console.error('No plugins rendered the detail', input);
+}