Developing Ruby Services for BrowserPlus
Ruby is a flexible,
expressive, complete, and small language that
is fun to write, and fairly straightforward to embed.
It allows programmers to do a whole lot in just a little code,
and has great out of the box APIs for scripting on the desktop.
In selecting
our first high level language to support, all of these factors
played into our choice of Ruby. Minimal BrowserPlus platform support
combined with the RubyInterpreter service make it possible
to write scriptable web plugins in Ruby. This page documents
the "Ruby Service API": how you go about writing services that
extend the web in Ruby.
How Ruby and BrowserPlus are Related
While we've never regretted our first choice of Ruby, we
didn't want to too tightly couple BrowserPlus to any one runtime
or interpreter. For this reason the Ruby integration is wholly
implemented in a BrowserPlus service. As this suggests, the
development
progression of the Ruby service authoring environment is orthogonal
to the progression of the platform. Further, the existence of
support for Ruby does not preclude other interpreted environments,
(say Python, lua, or php).
Enough with the theory, how about a little practice? The rest
of this page will cover the details of writing services in
Ruby.
Turning a Ruby Script into a BrowserPlus Service
The first question that you may have is "What's different
about a plain ol' Ruby script and a BrowserPlus service in Ruby?".
There are a couple key quirks and integration points:
- You must define a variable at the top level scope named
rubyCoreletDefinition which is a data structure that describes
the interface of your service.
- You must define a class, your "entry-point class" whose name is
contained in your rubyCoreletDefinition that implements the
functionality of your service.
- You may require Ruby standard libraries using the Kernel#require
method, as usual. However, to include additional Ruby files that you
package in your service, you must use the special bp_require
global function.
- You should _not_ use global variables. Instead use class variables
on your "entry-point class".
Defining Your Service's Interface
The information in the
rubyCoreletDefinition data structure
will be used to dynamically map functions into the browser that can be
invoked from Javascript, and will be used to validate that functions
are invoked with arguments of the correct type. This data structure
serves as the interface definition of your service.
The rubyCoreletDefinition is itself a hash with the following
top level (string, not symbol) keys:
- class - The name of the "entry-point class"
- name - The name of service as seen by Javascript clients
- major_version - The major version of your service which
should be incremented whenever API breaking changes are made.
- minor_version - The minor version of your service which
should be incremented whenever non-breaking API changes are made.
- micro_version - The micro version of your service which
should be incremented
whenever changes are made which do not affect the API (bug fixes,
performance improvements, or security fixes).
- documentation - A human readable english string which
describes to a web developer what they can do with this service.
- functions - An array of maps which define the functions that
the service exposes.
The
functions array is an array of maps, each describing a
function available on the entry-point class. The following top level
keys must be defined:
- name - The name of the function, This must match a function
present on your entry-point class.
- documentation - A human readable english string which
describes to a web developer what this function does. Also in
this description the return value should be documented.
- arguments - A human readable english string which
describes to a web developer what this function does. Also in
this description the return value of the functions should be
documented.
The
arguments array is an array of maps, each describing an
argument available on the function. For each map, the following top level
keys must be defined:
- name - The name of the parameter.
- documentation - A human readable english string which
describes to a web developer what this parameter does.
- type - The type of data that may be passed in this
parameter. Allowable types are "null", "bool", "integer",
"double", "string", "map", "list", or "path".
- required - A boolean which specifies whether the the
parameter is required or not.
The BrowserPlus platform will reject function invocations that do not
have required parameters, or pass the wrong types, so a degree of
argument validation is done for you.
Now let's have an example:
This definition defines a service called "MyGreatService" at version
"0.0.1". It provides a single function that a Javascript developer
may invoke, named "HelloWorld", that will return a string. This function
accepts a single required string argument named
"who" which is the name of the person to whom the service will say hello in
its return value.
Writing the "Entry-point class"
In your definition you specified a
class value which is the
name of the Ruby class that will implement your service's interface.
In the example above this
class was
MyGreatServiceInstance. When implementing that class
you must follow a couple rules:
- The name of the implementation class must match what's in your
definition
- The class must have an initialize member.
- The class must have one function for each function mentioned in
the definition
How about an example:
This simple "Entry-point" class combined with the description above
constitutes the complete Ruby implementation of a BrowserPlus service.
Arguments to #initialize
As you'll notice above, the initialize member of your entry point class
takes a single argument. This argument is a hash and contains several
string keys which provide information about the environment you're
running in and the browser client who is interacting with you. The
keys in the 'context' argument follow:
- uri is a string containing a URI of the current client. This is usually the full URL to the webpage that is interacting with browserplus. In the case of a native local application interacting with browserplus it should be a URI with a method of 'bpclient' (i.e. bpclient://CLIENTIDENTIFIER').
- corelet_dir DEPRECATED, use service_dir instead.
- service_dir is an absolute path to the directory containing the files distributed with the service.
- data_dir is a absolute path to where the service should store any data that needs to persist.
- temp_dir is a directory that may be used for temporary data. it will be unique for every instance, and will be purged when the instance is deallocated.
- locale The locale of the end user to which strings, if any, should be localized.
- userAgent The client user-agent string.
- clientPid The process ID of the client.
Arguments to Methods
Each method on your entry-point class which implements a function exposed
to Javascript will take two arguments:
- transaction which is an object that represents the current
transaction (or function invocation).
- arguments which is hash containing the arguments passed in
from Javascript.
The return value of your method will be ignored, and instead you should
return values by invoking the #complete method of the transaction object.
The primary reason for this pattern is to allow for the asynchronous
return of results. In general, none of your functions should sleep or
block, and Ruby threads should be used for long running or synchronous
operations.
The Transaction Object
The transaction object has two interesting methods:
- #complete A method that takes a single argument which will
be the return value passed to Javascript. This function completes
the transaction.
- #error A method that takes two optional arguments which
will end the transaction and return an error value to Javascript.
The first optional argument is an 'error code' that will be
passed up to Javascript. The second optional
argument is a verbose error code which should deliver more
information about what went wrong to the Javascript developer using
the service.
Calling #complete:
Calling #error:
Parameter Types
Thus far in examples we've only covered the
string parameter
type to services. Here we'll enumerate the full set of supported types
and how they are mapped into (and out of) Ruby:
- null - A parameter of type "null" may only have the
value of "null" (or "nil"). This is not a particularly
interesting parameter type.
- bool - Booleans hold a value of true or false.
- integer - Integers are represented in the platform as
64 bit signed integers, and may be bound to Fixnum or Bignum
in the Ruby environment, depending on the value.
- double - Double precision floating point integers, which
May be mapped into Float or Integers in the Ruby environment.
- string - UTF8 encoded strings
- map - Associative arrays or hashes. Note, it is not
possible to specify the types contained within a map, so the
service must do its own validation when accepting map parameters.
- list - Arrays. As with maps, it is not possible to specify
the types of data stored within maps, so the service must perform
validation.
- callback - A "callback" is currently represented as a
number in Ruby, and serves as an opaque handle to the Javascript
callback passed in. Callbacks are custom objects that will
be covered in the next section.
- path - Paths are handled specially in the BrowserPlus platform
for security reasons. The type of a path in Ruby is a Pathname
object. More discussion about paths and security implications
is available below.
Callback Arguments
A function on your service may accept arguments which are callbacks into
Javascript. Your function may define any number of these parameters and
may invoke them any number of times before calling #complete or #error on
the transaction object. Callbacks are passed into Ruby as objects which support a single function,
#invoke. The invoke function takes a single argument which
must consist of the basic types mentioned above and will be automatically
mapped into Javascript.
Calling into JavaScript with #invoke:
Breaking your Service Into Lots of Little Files
... Is generally a good thing. This is allowed and encouraged when
writing Ruby services, however there is one catch. For implementation
reasons the path to your installed service is not in the Ruby load path,
so
require cannot be used. A function is mapped into the
Ruby global context,
bp_require which allows you to require
Ruby libraries using paths relative to the directory into which your
service is installed.
Calling bp_require
Logging from your Service
A single global function (other than bp_require which we've
already covered) is available for use in the BrowserPlus Ruby environment.
This function allows you to log into the same logfile that BrowserPlus
uses. bp_log takes two arguments, the first is the log level, one of
'debug' 'info', 'warn', 'error', or 'fatal', and the second argument
is a string to log.
Calling bp_log:
The BrowserPlus logfile is stored in a platform dependent user scoped
location, and is a useful tool for understanding more about how
BrowserPlus works and debugging problems. The logfile can be found in
the following locations on different platforms:
OSX:
$HOME/Library/Application Support/Yahoo!/BrowserPlus/[platform version]/[uuid]/BrowserPlusCore.log
Windows XP:
%USERPROFILE%\Local Settings\Application Data\Yahoo!\BrowserPlus\[platform version]\[uuid]\BrowserPlusCore.log
Windows Vista:
%HOMEPATH%\AppData\LocalLow\Yahoo!\BrowserPlus\[platform version]\[uuid]\BrowserPlusCore.log
Note: [uuid] is a installation specific UUID that is generated at the time you install BrowserPlus.
Security concerns and the "path" type
Within the BrowserPlus platform, paths are special entities. Interacting
with the user's disk based files is a potentially dangerous activity which
needs special handling to ensure the user's data is not compromised.
For these reasons, except in very special cases, file paths should
always be accepted by services as the path type and not as the
string type.
Additionally, you should almost never return a path from your service.
The only way that a path may be made available to a webpage is if that
path was generated via user interaction, or if the
user is explicitly prompted about the types of paths your services
will provide to the webpage. This user interaction is a form of
implicit consent to manipulate a specific file. Two examples of
implicit user approval include dragging
and dropping the file onto the browser, or interacting
with a browse dialog and explicitly selecting files.
These security mechanisms are designed to minimize user risk, and as your
service gets ready for production we can work through security
considerations together.
Packaging and manifest.json
The final element of writing a service is the 'manifest' file. This is a
JSON file that contains metadata about your service. This file must
be named 'manifest.json'. Here is a complete example:
In general, the only lines you will need to change are 9, 13,
and 14: Containing the name of your top level Ruby script, an
end-user meaningful name for your service, and an end-user meaningful
description of what it does.