Integrating with Ruby on Rails

This sample shows how to integrate a SmartClient front-end with Ruby on Rails (also known as RoR or just "Rails").

The content discussed here will explore a way to integrate RoR with SmartClient, by leveraging SmartClient javascript API's on the front-end and MVC architecture in RoR web application on the back-end to generate the result sets. In this sample, a DataSource will be created which inherits from RestDataSource. This will get the data from the database using the smartclient gem and will serialize it back to the DataSource in JSON format. The results will be displayed in a list grid.

0. Prerequisites

  • Ruby on Rails framework, at least version 3.x (requires Ruby 1.8.6 or higher). It can be downloaded for free from here. Note that the smartclient gem has not yet been updated for Rails 4.
  • SmartClient SDK, at least version 8.3p. This can be downloaded from here.  Any edition is OK, including the free LGPL version.

1. Set up the application

(a) Create the Rails app

Once you have installed Rails on your development computer, first create the new Rails project with the command as follows:

rails new smartclient_app

(b) Install required gems

Next open the Gemfile in the project directory and add the 'smartclient' gem. The smartclient gem supports the various helper classes and methods for the SmartClient framework. We will discuss how the smartclient gem was written at the end of this article – you can also see the gem source on github.

Gemfile
gem "smartclient", "~> 0.0.7"

Then run the bundle command on the console:

bundle install

(c) Configure the Database

The database to use is specified in a configuration file, config/database.yml. If you open this file in a new Rails application, you’ll see a default database configured to use SQLite3. The file contains sections for three different environments in which Rails can run by default:

  • The development environment is used on your development/local computer as you interact manually with the application.
  • The test environment is used when running automated tests.
  • The production environment is used when you deploy your application for the world to use.

If you choose to use a different database, you will need to configure database.yml to your liking.

(d) Create the database

Now that you have your database configured, it’s time to have Rails create an empty database for you. You can do this by running a rake command:

rake db:create:all

Note that if you end up with an error that looks like ExecJS::RuntimeUnavailable, then you need to add a Javascript runtime to your Gemfile, for instance: 

Gemfile
 gem 'therubyracer'

 

2. Create Models, Migrations and Sample Data

(a) Create the models

First, generate some models for the sample data.

 

rails g model supplyitem
rails g model employee

The smartclient gem uses mass-assignment, so you will need to enable that in the models.


app/models/supplyitem.rb
class Supplyitem < ActiveRecord::Base
  attr_accessible :itemName, :sku, :description, :category, :units, :unitCost, :inStock, :nextShipment, :created_at, :updated_at
end
app/models/employee.rb
class Employee < ActiveRecord::Base
  attr_accessible :name, :reportsTo, :job, :email, :employeeType, :employeeStatus, :salary, :orgUnit, :gender, :maritalStatus, :created_at, :updated_at
end

(b) Define and Run Migrations

Then, define some migrations to set up the tables for the sample data. You can edit the blank migrations created for the models.

db/migrate/2014XXXX_create_suppyitems.rb
class CreateSupplyitems < ActiveRecord::Migration
  def change                   
    create_table :supplyitems do |t| 
      t.string   "itemName"
      t.string   "sku"
      t.string   "description"
      t.string   "category"
      t.string   "units"
      t.float    "unitCost"
      t.boolean  "inStock"
      t.datetime "nextShipment"
      t.timestamps
    end
  end
end
db/migrate/2014XXXX_create_employees.rb
class CreateEmployees < ActiveRecord::Migration
  def change                   
    create_table :employees do |t|
      t.string   "name"
      t.string   "reportsTo"
      t.string   "job"
      t.string   "email"
      t.string   "employeeType"
      t.string   "employeeStatus"
      t.float    "salary"
      t.string   "orgUnit"
      t.string   "gender"
      t.string   "maritalStatus"
      t.timestamps
    end
  end
end

Once you have defined the table structures, you can run the rake command to run the migrations and create the tables in the database.

rake db:migrate

(c) Generate sample data

In order to display the data in a ListGrid, the tables that were just created should have sample rows. You can create sample data by editing db/seeds.rb in the rails application, and then running a rake command.

seeds.rb
# This file should contain all the record creation needed to seed the database with its default values.
# The data can then be loaded with the rake db:seed (or created alongside the db with db:setup).
#
for count in 1..20
  Supplyitem.create do |item|
    item.itemName = "Pens stabiliner " + count.to_s
    item.sku = Random.rand(1000 ... 9999)
    item.description = "Description" + count.to_s
    item.category = "Office paper Product"
    item.units = "Ea"
    item.unitCost = Random.rand(0.1 ... 2)
    item.inStock = 1
    item.nextShipment = Time.now
  end

  Employee.create do |employee|
    employee.name = "Employee Name" + count.to_s
    employee.reportsTo = count.to_s
    employee.job = "Employee Job" + count.to_s 
    employee.email = "employee" + count.to_s + "@gmail.com"
    employee.employeeType = "EmployeeType" + count.to_s
    employee.employeeStatus = "EmployeeStatus" + count.to_s
    employee.salary = "Salary" + count.to_s
    employee.orgUnit = "OrgUnit" + count.to_s
    employee.gender = "1"
    employee.maritalStatus = "1"
  end
end

And you can run the command as follows;

rake db:seed

Finally you can start implementing with SmartClient.

3. Adding SmartClient libraries to Rails

Eventually, you may want to integrate the SmartClient javascript libraries with the Rails "assets" pipeline. However, initially, it is easier to serve them from the public directory of your Rails app.

(a) Copy the SmartClient runtime

Unzip your SmartClient SDK and copy the complete "isomorphic" folder from smartclientRuntime/isomorphic to the public directory in your application's folder.

(b) Add the SmartClient javascript to your layout page

On pages for which you want SmartClient available, you'll need to add the usual libraries to the layout page. Here, we do this for the default layout.

/app/views/layouts/application.html.erb
<!DOCTYPE html>
<html>
<head>
  <title>Smartclient</title>

  <script src="/isomorphic/system/modules/ISC_Core.js"></script>
  <script src="/isomorphic/system/modules/ISC_Foundation.js"></script>
  <script src="/isomorphic/system/modules/ISC_Containers.js"></script>
  <script src="/isomorphic/system/modules/ISC_Grids.js"></script>
  <script src="/isomorphic/system/modules/ISC_Forms.js"></script>
  <script src="/isomorphic/system/modules/ISC_DataBinding.js"></script>
  <script src="/isomorphic/skins/EnterpriseBlue/load_skin.js"></script>

  <%= stylesheet_link_tag    "application", :media => "all" %>
  <%= javascript_include_tag "application" %>
  <%= csrf_meta_tags %>
</head>
<body>

<%= yield %>

</body>
</html>

Adding these script tags will now ensure the master page will manage the loading of SmartClient, providing that this page is always defined as the master page for the view.

4. Create the Rails controller for data requests

We can use the rails command to create the new controller.

rails g controller smartclient

The Rails router recognizes URLs and dispatches them to a controller’s action. It can also generate paths and URLs, avoiding the need to hardcode strings in your views.

So after you have created the new controller, you should edit the routes.rb

/config/routes.rb
Smartclient::Application.routes.draw do 
  root to: 'smartclient#index'
  get 'smartclient/data'
end

You will also need to delete the default starting page at public/index.html, so that your root route will work.

5. Handling the SmartClient data with the gem

(a)  Handling a "fetch" request using RestDataSource

In order to handle SmartClient data requests, you will need to setup a RestDataSource on the client, and a mechanism to respond on the sever.

To setup a RestDataSource, you can put some Javascript code in the Rails asset pipeline. For instance, you could create app/assets/javascripts/datasource/supplyItem.js, with the following contents:

app/assets/javascripts/datasource/supplyItem.js
isc.RestDataSource.create({
    ID: "supplyItem",
    fields: [
        {name: "id", type: "sequence", hidden: true, primaryKey: true},
        {name: "itemName", type: "text", title: "Item", length: 128, required: true},
        {name: "sku", type: "text", title: "SKU", length: 10, required: true},
        {name: "description", type: "text", title: "Description", length: 2000},
        {name: "category", type: "text", title: "Category", length: 128, required: true},
        {name: "units", type: "enum", title: "Units", length: 5, valueMap: [
            "Roll", "Ea", "Pkt", "Set", "Tube", "Pad", "Ream", "Tin", "Bag", "Ctn", "Box"
        ]},
        {name: "unitCost", type: "float", title: "Unit Cost", required: true, validators: [
            {type: "floatRange", min: 0, errorMessage: "Please enter a valid (positive) cost"},
            {type: "floatPrecision", precision: 2, errorMessage: "The maximum allowed precision is 2"}
        ]},
        {name: "inStock", type: "boolean", title: "In Stock"},
        {name: "nextShipment", type: "date", title: "Next Shipment"}
    ],
  
    dataFormat: "json",
    dataURL: "/smartclient/data"
});

Note, the required format  is 'json' and the data url is set to 'smartclient/data', which means the controller name will need to be set to 'SmartclientController' and the method name will be 'fetch'. As everything else is unchanged, the DataSource will send the request parameters in the URL of the request.

Now, describe the controller to send back the response. You need to include the DSResponse.rb file in the header, since the DSResponse class defines the response structure in the smartclient gem.

/app/controllers/smartclient_controller.rb
require 'DSResponse'
class SmartclientController < ApplicationController         
    def index
       
    end
     
    def data
      # get all supplyitems from the database
      @supplyItems = Supplyitem.all.to_a
      # get the count of the supplyitems
      supplyitems_count = Supplyitem.count
      response = DSResponse.new
      response.data = @supplyItems
      response.startRow = 0
      response.endRow = supplyitems_count - 1
      response.status = 0
      response.totalRow = supplyitems_count      
             
      @result = { :response => response }
       
      render json: @result
    end
end

Then, to actually use this to draw a ListGrid, you can put some Javascript code in a Rails view. (Ultimately, you'll want to put this kind of code in the asset pipeline as well, but it's easy to illustrate using a view).

app/views/smartclient/index.html.erb
<SCRIPT>
isc.ListGrid.create({
    ID: "supplyItemGrid",
    width: 700, 
    height: 224,
    alternateRecordStyles: true,
    dataSource: "supplyItem",
    autoFetchData: true
}); 
</SCRIPT>

Now, you should be able to start the server:

rails s

And navigate to http://localhost:3000/ to see the results!

(b) Adding Simple Criteria, Sort, and Data Paging

First, change the ListGrid to enable data paging (with dataPageSize) and filtering and sorting (with filterEditor).

app/views/smartclient/index.html.erb
<SCRIPT>
isc.ListGrid.create({
    ID: "supplyItemGrid",
    width: 700,
    height: 224, 
    alternateRecordStyles: true,
    dataSource: "supplyItem",
    showFilterEditor: true,
    autoFetchData: true,
    dataPageSize: 10
});
</SCRIPT>

Additionally, change the DataSource definition to use POST instead of the default GET HTTP method used by normal fetch operations. This will post the JSON as a payload to the server instead of passing it the variables in a URL. To do this, add an operationBinding definition to the DataSource. The DataSource should now look like this:

app/assets/javascripts/datasource/supplyItem.js
isc.RestDataSource.create({
    ID: "supplyItem",
    fields: [
        {name: "id", type: "sequence", hidden: true, primaryKey: true},
        {name: "itemName", type: "text", title: "Item", length: 128, required: true},
        {name: "sku", type: "text", title: "SKU", length: 10, required: true},
        {name: "description", type: "text", title: "Description", length: 2000},
        {name: "category", type: "text", title: "Category", length: 128, required: true},
        {name: "units", type: "enum", title: "Units", length: 5, valueMap: [ 
            "Roll", "Ea", "Pkt", "Set", "Tube", "Pad", "Ream", "Tin", "Bag", "Ctn", "Box"
        ]},
        {name: "unitCost", type: "float", title: "Unit Cost", required: true, validators: [
            {type: "floatRange", min: 0, errorMessage: "Please enter a valid (positive) cost"},
            {type: "floatPrecision", precision: 2, errorMessage: "The maximum allowed precision is 2"}
        ]},
        {name: "inStock", type: "boolean", title: "In Stock"},
        {name: "nextShipment", type: "date", title: "Next Shipment"} 
    ],

    dataFormat: "json",
    operationBindings: [
        {operationType: "fetch", dataProtocol: "postMessage", dataURL: "/smartclient/data"}
    ]
});

If the sample,was run at this stage the payload sent to the server will look similar to this:

{
    dataSource: "supplyItem",
    operationType: "fetch",
    startRow: 0,
    endRow: 10,
    textMatchStyle: "substring",
    componentId: "supplyItemGrid",
    data: {
        units: "Pkt",
        unitCost: "a"
        ...
    },
    oldValues: null
}

The "smartclient" gem provides the DSRequest class to define the object which is created by de-serializing from JSON and which mimics the previous JSON. 

smartclient_controller.rb
require 'RPCManager'
class SmartclientController < ApplicationController             
    def index
      
    end
    
    def data 
      request = params[:smartclient]      
	  rpc = RPCManager.new(request) 	
	  rpc.model = Supplyitem
      @result = rpc.processRequest 
      render json: @result
    end           
end 

The RPCManager helper class which the smartclient gem provides can process the request. When the RPCManager object parses the request, we need to set its model attribute to the model class (here Supplyitem).

Since we've changed the RestDataSource to use post rather than get for data, we'll need to make the same change in config/routes.rb.

/config/routes.rb
Smartclient::Application.routes.draw do 
  root to: 'smartclient#index'
  post 'smartclient/data' 
end

Navigate to http://localhost:3000/ again to see the results.

(c) Adding other CRUD operations

In this part, the code will be refactored from the previous sample, the correct filtering functions will be implemented for the text columns, and additional functionality will be added to have a fully working DataSource implementation.

In the previous sample, filtering by a text value would not select rows which contained the filter term, only those that were specifically equal to it. To correctly filter by a text value, it is necessary to identify which columns are of type text and which are other types. 

First, add some additional operationBindings to the DataSource.

/app/assets/javascripts/datasource/supplyItem.js
isc.RestDataSource.create({
    ID: "supplyItem",
    fields: [
        {name: "id", type: "sequence", hidden: true, primaryKey: true},
        {name: "itemName", type: "text", title: "Item", length: 128, required: true},
        {name: "sku", type: "text", title: "SKU", length: 10, required: true},
        {name: "description", type: "text", title: "Description", length: 2000},
        {name: "category", type: "text", title: "Category", length: 128, required: true},
        {name: "units", type: "enum", title: "Units", length: 5, valueMap: [ 
            "Roll", "Ea", "Pkt", "Set", "Tube", "Pad", "Ream", "Tin", "Bag", "Ctn", "Box"
        ]},
        {name: "unitCost", type: "float", title: "Unit Cost", required: true, validators: [
            {type: "floatRange", min: 0, errorMessage: "Please enter a valid (positive) cost"},
            {type: "floatPrecision", precision: 2, errorMessage: "The maximum allowed precision is 2"}
        ]},
        {name: "inStock", type: "boolean", title: "In Stock"},
        {name: "nextShipment", type: "date", title: "Next Shipment"} 
    ],

    dataFormat: "json",        
    criteriaPolicy: "dropOnChange", 
    operationBindings: [       
        {operationType: "fetch", dataProtocol: "postMessage", dataURL: "/smartclient/data"},                
        {operationType: "add", dataProtocol: "postMessage", dataURL: "/smartclient/data"},
        {operationType: "update", dataProtocol: "postMessage", dataURL: "/smartclient/data"},
        {operationType: "remove", dataProtocol: "postMessage", dataURL: "/smartclient/data"}
    ] 
});

Now we should change the user interface for the CRUD operations.

/app/views/smartclient/index.html.erb
<SCRIPT>
isc.ListGrid.create({
    ID: "supplyItemGrid",
    width: 700,
    height: 224, 
    alternateRecordStyles: true,
    dataSource: "supplyItem",
    showFilterEditor: true,
    autoFetchData: true,
    dataPageSize: 20,
    canEdit: true,
    canRemoveRecords: true
});
isc.IButton.create({
    top: 250,
    title: "Edit New",
    click: "supplyItemGrid.startEditingNew()"
});
</SCRIPT>

The add, fetch, remove, update methods of the DataSource helper class of the gem are used for the CRUD through the RPCManager helper class.

Navigate to http://localhost:3000/ again to see the results.

(d) Add queuing and transaction support 

SmartClient has advanced features for queuing multiple requests in one single request. This provides a mechanism for sending multiple requests to the server in a single HTTP turnaround, thus minimizing network traffic as well as allowing the server to treat multiple requests as a single transaction (if the server is able to do so). 

You need to add the save button for the transaction progress.

app/views/smartclient/index.html.erb
<SCRIPT>
isc.ListGrid.create({
    ID: "supplyItemGrid",
    width: 700,
    height: 224,
    alternateRecordStyles: true,
    dataSource: "supplyItem",
    showFilterEditor: true,
    autoFetchData: true,
    dataPageSize: 20,
    canEdit: true,
    canRemoveRecords: true,
    autoSaveEdits: false
});
isc.IButton.create({
    top: 250,
    title: "Edit New",
    click: "supplyItemGrid.startEditingNew()"
});
isc.IButton.create({
    top: 250,
    left: 100,
    title: "Save all",
    click: "supplyItemGrid.saveAllEdits()"
});
</SCRIPT>

We don't need to define the controller again.

The processTransaction method of the RPCManager is used for the transaction.

(e)  Adding support for AdvancedCriteria

In this part, we will describe how to modify the FilterBuilder and the underlying AdvancedCriteria system, to build functionality resembling this showcase sample (but using our supplyItem DataSource, instead of the one in the webpage)

Modify app/assets/javascripts/smartclient_ui.js to include the relevant code for creating the FilterBuilder:

The ListGrid also requires additional code to add the FilterBuilder. This will require adding a vertical layout (VStack), together with the grid and the button needed to add for applying the filter on the ListGrid. Also going to add a horizontal layout (HStack) which will contain the two already existing buttons used for saving all data and creating a new record:

app/views/smartclient/index.html.erb
<SCRIPT>
isc.FilterBuilder.create({
    ID: "advancedFilter",
    dataSource: "supplyItem",
    topOperator: "and"
});
isc.ListGrid.create({
    ID: "supplyItemGrid",
    width: 700,
    height: 224,
    alternateRecordStyles: true,
    dataSource: "supplyItem",
    autoFetchData: true,
    dataPageSize: 20,
    canEdit: true,
    canRemoveRecords: true,
    autoSaveEdits: false
});
isc.IButton.create({
    ID: "filterButton",
    title: "Filter",
    click: function () {
        supplyItemGrid.filterData(advancedFilter.getCriteria());
    }
});
isc.HStack.create({
    membersMargin: 10,
    ID: "gridButtons",
    members: [
        isc.IButton.create({
            top: 250,
            title: "Edit New",
            click: "supplyItemGrid.startEditingNew()"
        }),
        isc.IButton.create({
            top: 250,
            left: 100,
            title: "Save all",
            click: "supplyItemGrid.saveAllEdits()"
        })
    ]
});
isc.VStack.create({
    membersMargin: 10,
    members: [advancedFilter, filterButton, supplyItemGrid, gridButtons]
});
</SCRIPT>

Also note, the filter has been removed top of the grid, as it is being replaced with the FilterBuilder.

The smartclient gem can parse the advanced criteria, if you take a look at the source code of the smartclient gem, you will find the fetch method in the DataSource.rb in the gem library direcotry, after you bundled the smartclient gem.

The AdvancedCriteria built by the FilterBuilder is sent in the JSON payload when doing a fetch() request. It is formatted like this:

AdvancedCriteria JSON Foramat
// an AdvancedCriteria
{
    "_constructor":"AdvancedCriteria",
    "operator":"and",
    "criteria":[
        // this is a Criterion
        { "fieldName":"salary", "operator":"lessThan", "value":"80000" },
        { "fieldName":"salary", "operator":"lessThan", "value":"80000" },
            ...  possibly more criterions ..
        { "operator":"or", "criteria":[
            { "fieldName":"title", "operator":"iContains", "value":"Manager" },
            { "fieldName":"reports", "operator":"notNull" }
            { "operator":"and", "criteria": [
                .. some more criteria or criterion here
            ]}
        ]}
        },
        { "operator":"or", "criteria":[
            { "fieldName":"title", "operator":"iContains", "value":"Manager" },
            { "fieldName":"reports", "operator":"notNull" }
        ]}
            .. possibly more criterions or criterias
    ]
}

As you can see it is a tree structure, with it's leafs being criterion and the nodes being criteria. If the the criteria member is null, then it is a leaf, otherwise it is a node which has sub-criterias or sub-criterions.

The buildCriterion method is called for this part.

Navigate to http://localhost:3000/ again to see the results.

(f)  Make it data-driven

This example takes the previous sample and makes it data driven and adds a way for the user to define new DataSource types. It will also be extended to define a new DataSource and change the user interface to allow the user to switch between the two DataSources.

We already defined the "employees" table so that we need to add the datasource.

app/assets/javascripts/datasource/employee.js
isc.RestDataSource.create({
    ID: "employee",
    fields: [
        {name: "id", type: "sequence", hidden: true, primaryKey: true}
        {name: "name", title: "Name", type: "text", length: 128},
        {name: "reportsTo", title: "Manager", type: "integer", required: "true", rootValue: 1, detail: true},
        {name: "job", title: "Title", type: "text", length: 128},
        {name: "email", title: "Email", type: "text", length: 128},
        {name: "employeeType", title: "Employee Type", type: "text", length: 40},
        {name: "employeeStatus", title: "Status", type: "text", length: 40},
        {name: "salary", title: "Salary", type: "float" },
        {name: "orgUnit", title: "Org Unit", type:"text", length: 128},
        {name: "gender", title: "Gender", type:"text", length: 7, valueMap: [
            "male", "female"
        ]},
        {name: "maritalStatus", title: "Marital Status", type: "text", length: 10, valueMap: [
            "married", "single"
        ]}
    ],
    dataFormat: "json",
    operationBindings: [
        {operationType: "fetch", dataProtocol: "postMessage", dataURL: "/smartclient/data"},                
        {operationType: "add", dataProtocol: "postMessage", dataURL: "/smartclient/data"},
        {operationType: "update", dataProtocol: "postMessage", dataURL: "/smartclient/data"},
        {operationType: "remove", dataProtocol: "postMessage", dataURL: "/smartclient/data"}
    ]
});

For the user interface, a change is required to allow users to switch the current DataSource. Add a form with a drop-down with the DataSources to switch and place it in front of the grids below the ListGrid. This requires putting the form in the HStack layout used for the buttons.

app/views/smartclient/index.html.erb
<SCRIPT>
isc.FilterBuilder.create({
    ID: "advancedFilter",
    dataSource: "supplyItem",
    topOperator: "and"
});
isc.ListGrid.create({
    ID: "supplyItemGrid",
    width: 700,
    height: 224,
    alternateRecordStyles: true,
    dataSource: "supplyItem",
    autoFetchData: true,
    dataPageSize: 20,
    canEdit: true,
    canRemoveRecords: true,
    autoSaveEdits: false
});
isc.IButton.create({
    ID: "filterButton",
    title: "Filter",
    click: function () {
        supplyItemGrid.filterData(advancedFilter.getCriteria());
    }
});
isc.HStack.create({
    membersMargin: 10,
    ID: "gridButtons",
    members: [
        isc.DynamicForm.create({
            values: {
                dataSource: "Change DataSource"
            },
            items: [{
                name: "dataSource",
                showTitle: false,
                editorType: "select",
                valueMap: ["supplyItem", "employee"],
                change: function (form, item, value, oldValue) {
                    if (!this.valueMap.contains(value)) {
                        return false;
                    } else {
                        supplyItemGrid.setDataSource(value);
                        advancedFilter.setDataSource(value);
                        supplyItemGrid.filterData(advancedFilter.getCriteria());
                    }
                }
            }]
        }),
        isc.IButton.create({
            top: 250,
            title: "Edit New",
            click: "supplyItemGrid.startEditingNew()"
        }),
        isc.IButton.create({
            top: 250,
            left: 100,
            title: "Save all",
            click: "supplyItemGrid.saveAllEdits()"
        })
    ]
});
isc.VStack.create({
    membersMargin: 10,
    members: [advancedFilter, filterButton, supplyItemGrid, gridButtons]
});
</SCRIPT>

Finally we should change the controller, when the user selects the method, we can get the model by get_datasource method of the RPCManager helper class.

 

smartclient_controller.rb
require 'RPCManager'
class SmartclientController < ApplicationController             
    def index
      
    end
    
    def data 
      request = params[:smartclient]
      rpc = RPCManager.new(request) 
      data_source = rpc.get_datasource
                 
      case data_source
      when 'supplyitem'
          # select supplyitems table
          model = Supplyitem
      when 'employee'
          # select employees table
          model = Employee          
      else
          # default
          model = Supplyitem
      end
      # set the table
      rpc.model = model
      # set the request parameters            
      @result = rpc.processRequest 
      render json: @result
    end           
end 

This example now shows a data-driven DataSource that allows users to add/remove/update two DataSources with two different entity DataSource, and also apply various filter criteria built with the Filter Builder.

Navigate to http://localhost:3000/ again to see the results.

6. smartclient gem 

As we explained about this gem before, this gem has the RPCManager, DSRequest, DSResponse, DataSource helper classes.

You can modify this gem and compile again by your requirement.

Now we will show the code of the smartclient gem.

1) DSResponse

This class makes the JSON payload for the front-end as the response.

DSResponse.rb
class DSResponse
	attr_accessor :data, :startRow, :endRow, :totalRow, :status  
	@data = nil
	@startRow = 0
	@endRow = 0
	@totalRow = 0
	@status = -1
	 
	def data=(value)
	  @data = value
	end
	 
	def startRow=(value)
	  @startRow = value
	end
	def endRow=(value)
	  @endtRow = value
	end
	def totalRow=(value)
	  @totalRow = value
	end
	def status=(value)
	  @status = value
	end 
end  

2) DSRequest

DSRequest.rb
require 'DataSource'
=begin
  <summary>
     reference to the RPCManager executing this request will be stored in DSRequest.
     This has to be done because, while the request is being.executed, access will be required to various items such as 
     the DataSource object, etc - These items will all be provided by the RPCManager class.
  </summary> 
=end
class DSRequest
    attr_accessor :dataSource, :operationType, :startRow, :endRow, :textMatchStyle, :data, :sortBy, :oldValues, :advancedCriteria
    
    @dataSource = nil
    @operationType = nil
    @startRow = nil
    @endRow = nil
    @textMatchStyle = nil
    @componentId = nil
    @data = nil             
    @sortBy = nil
    @oldValues = nil
    @advancedCriteria = nil       
    @@obj = nil      
    def initialize(data, model)
      @componentId = data[:componentId]
      @dataSource = data[:dataSource]
      @operationType = data[:operationType]
      @startRow = data[:startRow]
      @endRow = data[:endRow]
      @textMatchStyle = data[:textMatchStyle]
      @data = data[:data]
      @sortBy = data[:sortBy]
      @oldValues = data[:oldValues]
      
      @@obj = model
    end 
=begin
    <summary>
      The execute() method itself only loads the DataSource object then calls the DataSource's execute method for 
      processing the request.
    </summary>
      <params>
        @datasource: DataSource object from the RPCManager helper class
        @@obj: model object that is mapped to the table
      </params>  
=end
    def execute
      ds = DataSource.new(@dataSource, @@obj)
      if ds.nil?
        return nil
      else
        return ds.execute(self)
      end
    end
	
	def advancedCriteria=(value)
	  @advancedCriteria = value
	end
end

3) DataSource

DataSource.rb
=begin
  <summary>
    This helper classes process the request after recieve from the DSRequest.
    The CRUD methods(add, remove, update, fetch) were supported.      
  </summary>  
=end
class DataSource
  attr_accessor :data_source
  @data_source = nil
  @model = nil   
  @pk = nil
  def initialize(path, model)            
    @model = model
	@pk = @model.primary_key()	  
  end
=begin
  <summary> get the field content by the filed name </summary>  
=end  
  def get_field(field_name)
    fields = @data_source['fields']
    
    fields.each do | f |
      if f['name'] == filed_name
        return f
      end
    end     
    return nil
  end
=begin
  <summary> process the request </summary>  
=end
  def execute(request)
    operation_type = request.operationType      
      case operation_type
       when 'fetch' 
         @result = fetch(request)
       when 'add'         
         @result = add(request)
       when 'remove'
         @result = remove(request)
       when 'update'
         @result = update(request)
      end
    return @result
  end
  
private
	def buildStandardCriteria(request, table_name)
		query = 'SELECT * FROM ' + table_name + ' WHERE '
		param = Array.new    
		condition = '' 
		request.data.each do |key, value|
			condition += "#{key} LIKE ? AND "
			param << "%" + value + "%" 
		end
		q = condition[0, condition.rindex('AND ')]
		query += q
		
		order = ''
		unless request.sortBy.nil?
		  request.sortBy.each do |idx|
			 if idx.index('-') === nil 
				  order = " ORDER BY " + idx.to_s + " ASC"
			 else
				  order = " ORDER BY " + idx.to_s + " DESC"    
			 end
		  end
		end
		
		query += order
		temp = Array.new
		temp << query    
		temp.concat(param)
		return temp 
	end
	
	def buildAdvancedCriteria(request, table)
		advancedCriteria = request.advancedCriteria
		criteria_query = buildCriterion(advancedCriteria)		
		query = "SELECT * FROM " + table.to_s + " WHERE " + criteria_query[:query] 
		
		# sort by
		order = ''
		unless request.sortBy.nil?
		  request.sortBy.each do |idx|
			 if idx.index('-') === nil 
				  order = " ORDER BY " + idx.to_s + " ASC"
			 else
				  order = " ORDER BY " + idx.to_s + " DESC"    
			 end
		  end
		end
		query += order
		
		result = Array.new
		result << query   
		result.concat(criteria_query[:values])		
	
		return result
	end
	
	def buildCriterion(advancedCriteria)
		criterias = advancedCriteria[:criteria]
			
		operator = advancedCriteria[:operator]
		values = Array.new
		result = ''
		criterias.each do | c |  
			if c.has_key?(:fieldName)
				fn = c[:fieldName]
			end
			
			if c.has_key?(:operator)
				op = c[:operator]
			end
			
			if c.has_key?(:value)
				if c[:value] === true
					val = 1
				elsif c[:value] === false
					val = 0
				else
					val = c[:value]
				end
			end
			
			if c.has_key?(:start)	
				start = c[:start]
			end
			
			if c.has_key?(:end)
				_end = c[:end]
			end
			
			if c.has_key?(:criteria)
				criteria = c[:criteria]
			else
				criteria = nil
			end
			
			if criteria == nil				
				query = ''
				case op
					when 'equals'
						query = "#{fn} = ?"; 
						values << val
						
					when 'notEqual'
						query = "#{fn} != ?"; 
						values << val
						
					when 'iEquals'
						query = "UPPER(#{fn}) = ?"						
						values << "UPPER('#{val}')" 
						
					when 'iNotEqual'                            
						query = "UPPER(#{fn}) != ?"
						values << "UPPER('#{val}')"
						
					when 'greaterThan'
						query = "#{fn} > ?"		
						values << val
						
					when 'lessThan'
						query = "#{fn} < ?"		
						values << val
						
					when 'greaterOrEqual'
						query = "#{fn} >= ?"		
						values << val
						
					when 'lessOrEqual'
						query = "#{fn} <= ?"; 
						values << val
						
					when 'contains'
						query = "#{fn} LIKE ?";  
						values << "%#{val}%"
						
					when 'startsWith'
						query = "#{fn} LIKE ?";  
						values << "#{val}%"
						
					when 'endsWith'
						query = "#{fn} LIKE ?";  
						values << "%#{val}"
						
					when 'iContains'
						query = "#{fn} LIKE ?";  
						values << "%#{val}%"
						
					when 'iStartsWith'
						query = "UPPER(#{fn}) LIKE ?" 
						values << "UPPER('#{val}%')"
						
					when 'iEndsWith'
						query = "UPPER(#{fn}) LIKE ?" 
						values << "UPPER('%#{val}')"
							
					when 'notContains'
						query = "#{fn} NOT LIKE ?" 
						values << "%#{val}%"
							
					when 'notStartsWith'
						query = "#{fn} NOT LIKE ?" 
						values << "#{val}%"
						
					when 'notEndsWith'
						query = "#{fn} NOT LIKE ?" 
						values << "%#{val}"
						
					when 'iNotContains'
						query = "UPPER(#{fn}) NOT LIKE ?" 
						values << "UPPER('%#{val}%')"
						
					when 'iNotStartsWith'
						query = "UPPER(#{fn}) NOT LIKE ?" 
						values << "UPPER('#{val}%')"
												
					when 'iNotEndsWith'
						query = "UPPER(#{fn}) NOT LIKE ?" 
						values << "UPPER('%#{val}')"
						
					when 'isNull'
						query = "#{fn} IS NULL"
						
					when 'notNull'
						query = "#{fn} IS NOT NULL"
						
					when 'equalsField'
						query = "#{fn} LIKE ?"
						values << "CONCAT('#{val}', '%')"
						
					when 'iEqualsField'
						query = "UPPER(#{fn}) LIKE ?"
						values << "UPPER(CONCAT('#{val}', '%'))"
						
					when 'iNotEqualField'
						query = "UPPER(#{fn}) NOT LIKE ?"
						values << "UPPER(CONCAT('#{val}', '%'))"
						
					when 'notEqualField'
						query = "#{fn} NOT LIKE ?" 
						values << "CONCAT('#{val}', '%')"
						
					when 'greaterThanField'
						query = "#{fn} > ?"
						values << "CONCAT('#{val}', '%')"
						
					when 'lessThanField'
						query = "#{fn} < ?"
						values << "CONCAT('#{val}', '%')"
						
					when 'greaterOrEqualField'
						query = "#{fn} >= ?"
						values << "CONCAT('#{val}', '%')"
						
					when 'lessOrEqualField'
						query = "#{fn} <= ?"
						values << "CONCAT('#{val}', '%')"
						
					when 'iBetweenInclusive'
						query = "#{fn} BETWEEM ? AND ?"
						values << start
						values << _end
						
					when 'betweenInclusive'
						query = "#{fn} BETWEEM ? AND ?" 
						values << start
						values << _end
						
				end 	
				result = result.to_s + " " + query.to_s + " " + operator.to_s + " " 
			else
				# build the list of subcriterias or criterions                    
				temp = result
				result1 = buildCriterion(c)
				result = temp.to_s + "(" + result1[:query] + ") " + operator + " "
				
				result1[:values].each do | value |
					values << value
				end
			end 
		end 
		
		q = result[0, result.rindex(operator)]
		
		criteria_result = Hash.new
		criteria_result[:query] = q
		criteria_result[:values] = values
		
		return criteria_result 
	end
=begin
  <summary> get the item list from the table </summary>
  <note>Before this method is called, the filter method should define in the model of the projects.</note>  
=end  
	def fetch(request)      
		table_name = @model.table_name
		data = request.data
		# check the advanced cretira
		unless request.advancedCriteria.nil?			
			query = buildAdvancedCriteria(request, table_name)
			@obj_items = @model.find_by_sql(query) 
		else
			unless request.data.empty?
				query = buildStandardCriteria(request, table_name)
				@obj_items = @model.find_by_sql(query) 
			else
				@obj_items = @model.find(:all) 
			end
		end		 
		objs_count = @obj_items.count
		# get the count of the obj_items      
		endRow = (objs_count > 0)?objs_count - 1 : objs_count
		# make the Response result object 
		response = DSResponse.new
		response.data = @obj_items
		response.startRow = 0
		response.endRow = endRow
		response.status = 0
		response.totalRow = objs_count      
		return response 
    end
=begin
  <summary>Add new item</summary>  
=end     
    def add(request)      
      new_data = request.data	 
      @model.create(new_data)			
      response = DSResponse.new
      response.data = new_data
      response.status = 0      
      return response
    end
=begin
  <summary>Remove the selected item</summary>  
=end         
    def remove(request)      
      data = request.data      	  
	  id = data[@pk]	  
      # remove the item
      @model.destroy(id)	  
      response = DSResponse.new
      response.data = nil
      response.status = 0      
      return response 
    end
=begin
  <summary>Update the items</summary>  
=end         
    def update(request)      
      # get the old data from the request object
      old_data = request.oldValues
      # get the date from the request object
      update_data = request.data
	  	  
      new_id = update_data[@pk]
      # merge to hash objects      
      merged_data = old_data.merge!(update_data)      
      merged_data.delete(@pk)
      
      #update
      @model.update(new_id, merged_data)      	  
	  response = DSResponse.new      
      response.status = 0      
      return response
    end
end

4) RPCManager

RPCManager.rb
=begin
  <summary>
    Any action of the user with the DataSource will only call the RPCManager and will delegate all responsibility to it. 
    The RPCManager will parse the payload and setup the DSRequest request and will call for the request's execute() method
     which will return the DSResponse object. The RPCManager will then convert this DSResponse into a suitable response 
     and return it to the front-end.  
   </summary>
=end
require 'DSRequest'
require 'DSResponse'
class RPCManager
  @request = nil  
  @model = nil
  @temp_request = nil
=begin
  <summary>
    Process the request with the model.
  </summary>  
  
  <params>
    request: posted request parameters
    model: the object that is mapped to the table
  </params>
=end
  def initialize(request=nil)        
	# if is not wrapped in a transaction then we'll wrap it to make unified handling of the request	
	if !check_transaction(request)		
		req_hash = HashWithIndifferentAccess.new
		req_hash[:transaction] = HashWithIndifferentAccess.new
		req_hash[:transaction][:transactionNum] = -1 				
		req_list = Array.new
		req_list << request
		req_hash[:transaction][:operations] = req_list
		@request = req_hash
	else
		@request = request
	end	 
  end  
  
  def model=(model)
	@model = model
  end
=begin
	<summary>
		Helper method to decide if request contains an advanced criteria or not
    </summary>
		<param name="req"></param>
    <returns></returns>
=end  
	def check_advanced_criteria(data)	 
		if data.include?(:_constructor)
			return true
		else
			return false
		end
	end
=begin
	<summary>
		Returns true if the request has transaction support
    </summary>
    <returns></returns>
=end
	def check_transaction(request)
		if request.include?(:transaction)# and request.include?(:operations) and request.include?(:transactionNum) 
			return true
		else
			return false
		end
	end
=begin
  <summary>
      Transforms a object object into a Json. Will setup the serializer with the        
      appropriate converters, attributes,etc.
  </summary>
      <param name="dsresponse">the object object to be transformed to json</param>
      <returns>the created json object</returns>
=end
  def processRequest
	response = processTransaction   	
	@result = { :response => response } 
    return @result 	
  end
=begin
	<summary>
		Select the model by the datasource
    </summary>
    <returns>Datasource name</returns>
=end  
	def get_datasource
		if @request.include?(:transaction)
			return @request[:transaction][:operations][0][:dataSource]
		else
			return @request[:dataSource]
		end
	end
=begin
	<summary>
		Process the transaction request for which this RPCManager was created for
    </summary>
    <returns></returns>
=end
	def processTransaction		
		# retrieve the requests with data in form
		transaction_request = @request[:transaction] 		
		# store transaction num, we'll use it later to see if there was a transaction or not
		transaction_num = transaction_request[:transactionNum]
		# fetch the operations
		operations = transaction_request[:operations]
		
		# response list
		res_list = Array.new			
		# transaction progress
		@model.transaction do 									
			begin				
				operations.each do |op|	 
					# parase advanced criterias, if any
					advanced_criteria = parse_advanced_criterias(op)
					 
					req = DSRequest.new(op, @model) 					
					unless advanced_criteria == nil
						req.advancedCriteria = advanced_criteria
					end 
					# execute the request and get the response
					res = req.execute							 
					if res == nil
						res = DSResponse.new
						res.status = -1
					end
					
				
						
					# store the response for later
					res_list << res	 					
				end			
			rescue ActiveRecord::RecordInvalid
				# if it occurs exception
				raise ActiveRecord::Rollback
			end
		end
		
		# if we have only one object, send directly the DSResponse
		if transaction_num  == -1
			response = DSResponse.new
			response.data = res_list[0].data
			response.startRow = res_list[0].startRow
			response.endRow = res_list[0].endRow
			response.totalRow = res_list[0].totalRow
			response.status = res_list[0].status
			
			return response
		end
		
		# iterate over the responses and create a instance of an anonymous class which mimics the required json
		responses = Array.new
		
		res_list.each do | response |			
			res = DSResponse.new
			res.data = response.data			
			res.startRow = response.startRow
			res.endRow = response.endRow
			res.totalRow = response.totalRow
			res.status = response.status			
			responses << res
		end 
		return responses
	end
	
	def parse_advanced_criterias(operations)
		data = operations[:data]			
		if check_advanced_criteria(data)
			return data
		end
		return nil		
	end
end

 

You can find this gem on the rubygems sites. (https://rubygems.org/gems/smartclient) and you can review the source code from github or document of the rubygems site(http://rubydoc.info/gems/smartclient/0.0.7/frames).