RhoSync is a synchronization framework consisting of a client component on the device and a server component that runs on any server capable of running Ruby. RhoSync requires that you write a small amount of Ruby code for the query, create, update and delete operations of your particular enterprise backend. The collection of the Ruby code for these operations we refer to as a “source” or “source adapter”. Full documentation of all capabilities of RhoSync is in the RhoSync Developer Reference.
Install RhoSync as described here.
If you have a RhoSync license that has been sent to you you can replace the settings/license.yml file as described here.
Now you are ready to write your RhoSync app. You can generate an app with the RhoSync app command.
$ rhosync app storemanager-server $ cd storemanager-server/
If you are running this for the first time on Mac or Linux, you will need to install dtach:
$ sudo rake dtach:install
Now you can run redis and your RhoSync app:
$ rake redis:start $ rake rhosync:start
If everything went well you should see:
[07:01:15 PM 2010-05-04] Rhosync Server v2.1.0 started...
You will be prompted to edit the session secret in your config.ru to something other than “\<changeme>”
Once RhoSync is installed we’re ready to build a RhoSync source to integrate with our backend application. To define a RhoSync source you just need to identify a handful of operations to interact with your backend data source: login, query, sync, create, update, delete and logoff. For more information please see the RhoSync source adapter documentation.
From the storemanager-sever directory, generate a source adapter for the product model:
$ rhosync source product
which generates two files, the product adapter and the product spec:
Generating with source generator: [ADDED] sources/product.rb [ADDED] spec/sources/product_spec.rb
You’ll see a file similar to the following one below. The generated source adapter has code to raise an exception for any required method. Note that you don’t need to use the source generator. You can just create a Ruby file and place it into your lib directory. The class name of the source adapter must match that of the client model.
class Product < SourceAdapter def initialize(source,credential) super(source,credential) end def login # TODO: Login to your data source here if necessary end def query # TODO: Query your backend data source and assign the records # to a nested hash structure called @result. For example: # @result = { # "1"=>{"name"=>"Acme", "industry"=>"Electronics"}, # "2"=>{"name"=>"Best", "industry"=>"Software"} # } raise SourceAdapterException.new("Please provide some code to read records from the backend data source") end def sync # Manipulate @result before it is saved, or save it # yourself using the Rhosync::Store interface. # By default, super is called below which simply saves @result super end def create(create_hash,blob=nil) # TODO: Create a new record in your backend data source # If your rhodes rhom object contains image/binary data # (has the image_uri attribute), then a blob will be provided raise "Please provide some code to create a single record in the backend data source using the create_hash" end def update(update_hash) # TODO: Update an existing record in your backend data source raise "Please provide some code to update a single record in the backend data source using the update_hash" end def delete(object_id) # TODO: write some code here if applicable # be sure to have a hash key and value for "object" # for now, we'll say that its OK to not have a delete operation # raise "Please provide some code to delete a single object in the backend application using the hash values in name_value_list" end def logoff # TODO: Logout from the data source if necessary end end
The generator will also edit settings/settings.yml and add the product adapter to the sources section with some default options:
#Sources :sources: Product: :poll_interval: 300
The next step is for you to fill in the login, query, create, update, delete and logoff methods with your own code to call a backend service.
The description below shows what such code might look like.
If you’re doing a readonly non-authenticated source adapter, you can get away with just writing one method, query, to retrieve records as we describe here. The following is a sample query method to interact with a simple product catalog (available at http://rhostore.heroku.com) that exposes a REST interface. Note that RhoSync can work with any protocol. This example simply shows JSON over HTTP with a REST interface since that is common. The RhoSync source adapter is pure Ruby code and there are ruby libraries (aka gems) that will make it easy to connect to and parse whatever you need – the query code would just be slightly different.
Our sample web service for returning all products in the catalog (http://rhostore.heroku.com/products.json) returns data like this:
[ { "product": { "name": "iPhone", "brand": "Apple", "updated_at": "2010-05-11T02:04:57Z", "price": "$299.99", "quantity": "5", "id": 649, "sku": "1234", "created_at": "2010-05-11T02:04:57Z" } }, { "product": { "name": "Accord", "brand": "Honda", "updated_at": "2010-05-13T22:24:48Z", "price": "$6000", "quantity": "", "id": 648, "sku": "123", "created_at": "2010-05-11T02:04:53Z" } } ]
The Ruby code for parsing that data listed below uses the standard Ruby JSON library and the RestClient library for easy access to the REST web service. This example uses the id of the product record as the hash key. Note that the key for this hash must be a string and the value can be any set of name-value pairs which are represented as a Ruby hash. The instance variable @result must be set by the query method to this “hash of hashes”, indexed by a unique identifier, so that the base SourceAdapter class sync method can populate Redis data store.
We need to declare the standard libraries that we are using at the top of the file:
require 'json' require 'rest_client'
For convenience, we’ll add an instance variable @base which contains the base URL of the web service and set the value in the constructor:
def initialize(source,credential) @base = 'http://rhostore.heroku.com/products' super(source,credential) end
Then fill in the query method:
def query parsed=JSON.parse(RestClient.get("#{@base}.json").body) @result={} if parsed parsed.each do |item| key = item["product"]["id"].to_s @result[key]=item["product"] end end end
**
The code above could be much more concise, but it is written to be easily readable by programmers who are unfamiliar with the Ruby language. If you are new to Ruby, you can read Ruby from other languages for a good introduction. |
Each hash key in the inner hash represents an attribute of an individual object. All datatypes must be strings (so the hash values need to all be strings not integers).**
For example:
@result = { "1" => { "name" => "inner tube", "brand" => "Michelin" }, "2" => { "name" => "tire", "brand" => "Michelin" } }
Make sure you are running redis and start (or restart) your server:
$ rake rhosync:start
The code for the source adapter loads when the server starts. If you have a syntax error in your Ruby code, it will be reported and the server will not start; however, if you have a runtime error, that will not be reported until the source adapter is called.
Make sure your server URL is configured in the app. If your server is running on localhost with the default port, the following line should be at the bottom of rhoconfig.txt:
syncserver = 'http://localhost:9292/application'
Enable sync in your storemanager/app/Product/product.rb model:
class Product include Rhom::PropertyBag enable :sync end
To get a feel for what is happening, it is helpful to watch the server log (the output of rake rhosync:start) in one window, and tail the client log in another window. For example, on the iPhone, display the end of the client log with:
$ tail -f rholog-User.txt
To sync with the server, the client must log in. The generated app includes some screens for login and other common functions, which you will typically modify to suit the design of your application. The generated UI is useful since it allows you to focus on the core functionality of your application before implementing the important, but mundane, details of user authentication and settings.
For your create method you can assume that the RhoSync server will pass you a hash containing the new record called “create_hash”. For example it might be:
{ "name" => "Hovercraft", "brand" => "Acme" }
The create method will be called once for every object that has been created on the client since the last sync. Your code for create (or edit or delete) needs to use this populated array to do its work. Below is an example of a create method against the rhostore, which accepts an HTTP POST to perform a create action. The create method must return a unique id string for the object for it to be later modifiable by the client. If no id is returned, then you should treat the client object as read only, because it will not be able to be synchronized.
def create(create_hash, blob=nil) result = RestClient.post(@base,:product => new_object_hash) # after create we are redirected to the new record. # The URL of the new record is given in the location header location = "#{result.headers[:location]}.json" # We need to get the id of that record and return it as part of create # so rhosync can establish a link from its temporary object on the # client to this newly created object on the server new_record = RestClient.get(location).body JSON.parse(new_record)["product"]["id"].to_s end
You will need to restart RhoSync to reload the source adapter after modifying code. Note that the object will be created on the client right away, but it will be sent to the server on the next sync.
The generated application code includes a file at the root of the directory called “application.rb” which contains a hook for authentication. The complete file looks like this:
class Application < Rhosync::Base class << self def authenticate(username,password,session) true # do some interesting authentication here... end # Add hooks for application startup here # Don't forget to call super at the end! def initializer(path) super end # Calling super here returns rack tempfile path: # i.e. /var/folders/J4/J4wGJ-r6H7S313GEZ-Xx5E+++TI # Note: This tempfile is removed when server stops or crashes... # See http://rack.rubyforge.org/doc/Multipart.html for more info # # Override this by creating a copy of the file somewhere # and returning the path to that file (then don't call super!): # i.e. /mnt/myimages/soccer.png def store_blob(blob) super #=> returns blob[:tempfile] end end end Application.initializer(ROOT_PATH)
If your back end web service requires authentication, simply add code to the authenticate method and return true if authentication was successful or false to deny access to the application from this client. For example:
def authenticate(username, password, session) # ... connect to backend using API and authenticate ... if success # save the data for later use in the source adapter Store.put_value("username:#{username}:token",username) end return success end
You don’t have to use Rhodes to use RhoSync. For this scenario, we offer an Objective C client for RhoSync.