Merge branch 'develop'
diff --git a/.travis.yml b/.travis.yml
index 1849328..34f69c5 100644
--- a/.travis.yml
+++ b/.travis.yml
@@ -3,4 +3,4 @@
   - 1.9.3
   - 2.1.1
 script:
-  - rspec
+  - rake
diff --git a/lib/predictionio.rb b/lib/predictionio.rb
index 73250bd..9879266 100644
--- a/lib/predictionio.rb
+++ b/lib/predictionio.rb
@@ -1,4 +1,3 @@
-require 'predictionio/client'
 require 'predictionio/event_client'
 require 'predictionio/engine_client'
 
diff --git a/lib/predictionio/client.rb b/lib/predictionio/client.rb
deleted file mode 100644
index e2b405b..0000000
--- a/lib/predictionio/client.rb
+++ /dev/null
@@ -1,703 +0,0 @@
-# Ruby SDK for convenient access of PredictionIO Output API.
-#
-# Author::    TappingStone (help@tappingstone.com)
-# Copyright:: Copyright (c) 2013 TappingStone
-# License::   Apache License, Version 2.0
-
-require 'date'
-require 'json'
-require 'net/http'
-require 'predictionio/async_request'
-require 'predictionio/async_response'
-require 'predictionio/connection'
-
-module PredictionIO
-  # This class contains methods that access PredictionIO via REST requests.
-  #
-  # Many REST request methods support optional arguments.
-  # They can be supplied to these methods as Hash'es.
-  # For a complete reference, please visit http://prediction.io.
-  #
-  # == High-performance Asynchronous Backend
-  #
-  # All REST request methods come in both synchronous and asynchronous flavors.
-  # Both flavors accept the same set of arguments.
-  # In addition, all synchronous request methods can instead accept a PredictionIO::AsyncResponse object generated from asynchronous request methods as its first argument.
-  # In this case, the method will block until a response is received from it.
-  #
-  # Any network reconnection and request retry is automatically handled in the background.
-  # Exceptions will be thrown after a request times out to avoid infinite blocking.
-  #
-  # == Special Handling of Some Optional Arguments
-  # Some optional arguments have additional special handling:
-  # - For all requests that accept "itypes" as input, the value can be supplied as either an Array of String's, or a comma-delimited String.
-  # - For all requests that accept "pio_latlng" as input, they will also accept "pio_latitude" and "pio_longitude".
-  #   When these are supplied, they will override any existing "pio_latlng" value.
-  # - All time arguments (e.g. t, pio_startT, pio_endT, etc) can be supplied as either a Time or Float object.
-  #   When supplied as a Float, the SDK will interpret it as a UNIX UTC timestamp in seconds.
-  #   The SDK will automatically round to the nearest millisecond, e.g. 3.14159 => 3.142.
-  #
-  # == Installation
-  # The easiest way is to use RubyGems:
-  #     gem install predictionio
-  #
-  # == Synopsis
-  # The recommended usage of the SDK is to fire asynchronous requests as early as you can in your code,
-  # and check results later when you need them.
-  #
-  # === Instantiate PredictionIO Client
-  #     # Include the PredictionIO SDK
-  #     require "predictionio"
-  #
-  #     client = PredictionIO::Client.new(<appkey>)
-  #
-  # === Import a User Record from Your App (with asynchronous/non-blocking requests)
-  #
-  #     #
-  #     # (your user registration logic)
-  #     #
-  #
-  #     uid = get_user_from_your_db()
-  #
-  #     # PredictionIO call to create user
-  #     response = client.acreate_user(uid)
-  #
-  #     #
-  #     # (other work to do for the rest of the page)
-  #     #
-  #
-  #     begin
-  #       # PredictionIO call to retrieve results from an asynchronous response
-  #       result = client.create_user(response)
-  #     rescue UserNotCreatedError => e
-  #       log_and_email_error(...)
-  #     end
-  #
-  # === Import a User Action (Rate) from Your App (with synchronous/blocking requests)
-  #     # PredictionIO call to record the view action
-  #     begin
-  #       client.identify("foouser")
-  #       result = client.record_action_on_item("rate", "baritem", "pio_rate" => 4)
-  #     rescue U2IActionNotCreatedError => e
-  #       ...
-  #     end
-  #
-  # === Retrieving Top N Recommendations for a User
-  #     # PredictionIO call to get recommendations
-  #     client.identify("foouser")
-  #     response = client.aget_itemrec_top_n("barengine", 10)
-  #
-  #     #
-  #     # work you need to do for the page (rendering, db queries, etc)
-  #     #
-  #
-  #     begin
-  #       result = client.get_itemrec_top_n(response)
-  #       # display results, store results, or your other work...
-  #     rescue ItemRecNotFoundError => e
-  #       # graceful error handling
-  #     end
-  #
-  # === Retrieving Top N Similar Items for an Item
-  #     # PredictionIO call to get similar items
-  #     response = client.aget_itemsim_top_n("barengine", "fooitem", 10)
-  #
-  #     #
-  #     # work you need to do for the page (rendering, db queries, etc)
-  #     #
-  #
-  #     begin
-  #       result = client.get_itemsim_top_n(response)
-  #       # display results, store results, or your other work...
-  #     rescue ItemSimNotFoundError => e
-  #       # graceful error handling
-  #     end
-
-  class Client
-
-    # Appkey can be changed on-the-fly after creation of the client.
-    attr_accessor :appkey
-
-    # Only JSON is currently supported as API response format.
-    attr_accessor :apiformat
-
-    # The UID used for recording user-to-item actions and retrieving recommendations.
-    attr_accessor :apiuid
-
-    # Raised when a user is not created after a synchronous API call.
-    class UserNotCreatedError < StandardError; end
-
-    # Raised when a user is not found after a synchronous API call.
-    class UserNotFoundError < StandardError; end
-
-    # Raised when a user is not deleted after a synchronous API call.
-    class UserNotDeletedError < StandardError; end
-
-    # Raised when an item is not created after a synchronous API call.
-    class ItemNotCreatedError < StandardError; end
-
-    # Raised when an item is not found after a synchronous API call.
-    class ItemNotFoundError < StandardError; end
-
-    # Raised when an item is not deleted after a synchronous API call.
-    class ItemNotDeletedError < StandardError; end
-
-    # Raised when ItemRec results cannot be found for a user after a synchronous API call.
-    class ItemRecNotFoundError < StandardError; end
-
-    # Raised when ItemRank results cannot be found for a user after a synchronous API call.
-    class ItemRankNotFoundError < StandardError; end
-
-    # Raised when ItemSim results cannot be found for an item after a synchronous API call.
-    class ItemSimNotFoundError < StandardError; end
-
-    # Raised when an user-to-item action is not created after a synchronous API call.
-    class U2IActionNotCreatedError < StandardError; end
-
-    # Create a new PredictionIO client with default:
-    # - 10 concurrent HTTP(S) connections (threads)
-    # - API entry point at http://localhost:8000 (apiurl)
-    # - a 60-second timeout for each HTTP(S) connection (thread_timeout)
-    def initialize(appkey, threads = 10, apiurl = "http://localhost:8000", thread_timeout = 60)
-      @appkey = appkey
-      @apiformat = "json"
-      @http = PredictionIO::Connection.new(URI(apiurl), threads, thread_timeout)
-    end
-
-    # Returns the number of pending requests within the current client.
-    def pending_requests
-      @http.packages.size
-    end
-
-    # Returns PredictionIO's status in string.
-    def get_status
-      status = @http.aget(PredictionIO::AsyncRequest.new("/")).get()
-      begin
-        status.body
-      rescue Exception
-        status
-      end
-    end
-
-    # :category: Asynchronous Methods
-    # Asynchronously request to create a user and return a PredictionIO::AsyncResponse object immediately.
-    #
-    # Corresponding REST API method: POST /users
-    #
-    # See also #create_user.
-    def acreate_user(uid, params = {})
-      rparams = params
-      rparams["pio_appkey"] = @appkey
-      rparams["pio_uid"] = uid
-      if params["pio_latitude"] && params["pio_longitude"]
-        rparams["pio_latlng"] = "#{params["pio_latitude"]},#{params["pio_longitude"]}"
-      end
-
-      @http.apost(PredictionIO::AsyncRequest.new("/users.#{@apiformat}", rparams))
-    end
-
-    # :category: Synchronous Methods
-    # Synchronously request to create a user and block until a response is received.
-    #
-    # See also #acreate_user.
-    #
-    # call-seq:
-    # create_user(uid, params = {})
-    # create_user(async_response)
-    def create_user(*args)
-      uid_or_res = args[0]
-      if uid_or_res.is_a?(PredictionIO::AsyncResponse)
-        response = uid_or_res.get
-      else
-        uid = uid_or_res
-        response = acreate_user(*args).get
-      end
-      unless response.is_a?(Net::HTTPCreated)
-        begin
-          msg = response.body
-        rescue Exception
-          raise UserNotCreatedError, response
-        end
-        raise UserNotCreatedError, msg
-      end
-    end
-
-    # :category: Asynchronous Methods
-    # Asynchronously request to get a user and return a PredictionIO::AsyncResponse object immediately.
-    #
-    # Creation time of the user will be returned as a Time object.
-    #
-    # If the result contains a latlng key, both latitude and longitude will also be available as separate keys.
-    #
-    # Corresponding REST API method: GET /users/:uid
-    #
-    # See also #get_user.
-    def aget_user(uid)
-      @http.aget(PredictionIO::AsyncRequest.new("/users/#{uid}.#{@apiformat}",
-                                                "pio_appkey" => @appkey,
-                                                "pio_uid" => uid))
-    end
-
-    # :category: Synchronous Methods
-    # Synchronously request to get a user and block until a response is received.
-    #
-    # Creation time of the user will be returned as a Time object.
-    #
-    # If the result contains a latlng key, both latitude and longitude will also be available as separate keys.
-    #
-    # See also #aget_user.
-    #
-    # call-seq:
-    # get_user(uid)
-    # get_user(async_response)
-    def get_user(uid_or_res)
-      if uid_or_res.is_a?(PredictionIO::AsyncResponse)
-        response = uid_or_res.get
-      else
-        response = aget_user(uid_or_res).get
-      end
-      if response.is_a?(Net::HTTPOK)
-        res = JSON.parse(response.body)
-        if res["pio_latlng"]
-          latlng = res["pio_latlng"]
-          res["pio_latitude"] = latlng[0]
-          res["pio_longitude"] = latlng[1]
-        end
-        res
-      else
-        begin
-          msg = response.body
-        rescue Exception
-          raise UserNotFoundError, response
-        end
-        raise UserNotFoundError, msg
-      end
-    end
-
-    # :category: Asynchronous Methods
-    # Asynchronously request to delete a user and return a PredictionIO::AsyncResponse object immediately.
-    #
-    # Corresponding REST API method: DELETE /users/:uid
-    #
-    # See also #delete_user.
-    def adelete_user(uid)
-      @http.adelete(PredictionIO::AsyncRequest.new("/users/#{uid}.#{@apiformat}",
-                                                   "pio_appkey" => @appkey,
-                                                   "pio_uid" => uid))
-    end
-
-    # :category: Synchronous Methods
-    # Synchronously request to delete a user and block until a response is received.
-    #
-    # See also #adelete_user.
-    #
-    # call-seq:
-    # delete_user(uid)
-    # delete_user(async_response)
-    def delete_user(uid_or_res)
-      if uid_or_res.is_a?(PredictionIO::AsyncResponse)
-        response = uid_or_res.get
-      else
-        response = adelete_user(uid_or_res).get
-      end
-      unless response.is_a?(Net::HTTPOK)
-        begin
-          msg = response.body
-        rescue Exception
-          raise UserNotDeletedError, response
-        end
-        raise msg
-      end
-    end
-
-    # :category: Asynchronous Methods
-    # Asynchronously request to create an item and return a PredictionIO::AsyncResponse object immediately.
-    #
-    # Corresponding REST API method: POST /items
-    #
-    # See also #create_item.
-    def acreate_item(iid, itypes, params = {})
-      rparams = params
-      rparams["pio_appkey"] = @appkey
-      rparams["pio_iid"] = iid
-      begin
-        rparams["pio_itypes"] = itypes.join(",")
-      rescue Exception
-        rparams["pio_itypes"] = itypes
-      end
-      if params["pio_latitude"] && params["pio_longitude"]
-        rparams["pio_latlng"] = "#{params["pio_latitude"]},#{params["pio_longitude"]}"
-      end
-      rparams["pio_startT"] = ((params["pio_startT"].to_r) * 1000).round(0).to_s if params["pio_startT"]
-      rparams["pio_endT"]   = ((params["pio_endT"].to_r) * 1000).round(0).to_s if params["pio_endT"]
-
-      @http.apost(PredictionIO::AsyncRequest.new("/items.#{@apiformat}", rparams))
-    end
-
-    # :category: Synchronous Methods
-    # Synchronously request to create an item and block until a response is received.
-    #
-    # See #acreate_item for a description of other accepted arguments.
-    #
-    # call-seq:
-    # create_item(iid, itypes, params = {})
-    # create_item(async_response)
-    def create_item(*args)
-      iid_or_res = args[0]
-      if iid_or_res.is_a?(PredictionIO::AsyncResponse)
-        response = iid_or_res.get
-      else
-        response = acreate_item(*args).get
-      end
-      unless response.is_a?(Net::HTTPCreated)
-        begin
-          msg = response.body
-        rescue Exception
-          raise ItemNotCreatedError, response
-        end
-        raise ItemNotCreatedError, msg
-      end
-    end
-
-    # :category: Asynchronous Methods
-    # Asynchronously request to get an item and return a PredictionIO::AsyncResponse object immediately.
-    #
-    # Creation time of the user will be returned as a Time object.
-    #
-    # If the result contains a latlng key, both latitude and longitude will also be available as separate keys.
-    #
-    # Corresponding REST API method: GET /items/:iid
-    #
-    # See also #get_item.
-    def aget_item(iid)
-      @http.aget(PredictionIO::AsyncRequest.new("/items/#{iid}.#{@apiformat}",
-                                                "pio_appkey" => @appkey,
-                                                "pio_iid" => iid))
-    end
-
-    # :category: Synchronous Methods
-    # Synchronously request to get an item and block until a response is received.
-    #
-    # Creation time of the item will be returned as a Time object.
-    #
-    # If the result contains a latlng key, both latitude and longitude will also be available as separate keys.
-    #
-    # See also #aget_item.
-    #
-    # call-seq:
-    # get_item(iid)
-    # get_item(async_response)
-    def get_item(iid_or_res)
-      if iid_or_res.is_a?(PredictionIO::AsyncResponse)
-        response = iid_or_res.get
-      else
-        response = aget_item(iid_or_res).get
-      end
-      if response.is_a?(Net::HTTPOK)
-        res = JSON.parse(response.body)
-        if res["pio_latlng"]
-          latlng = res["pio_latlng"]
-          res["pio_latitude"] = latlng[0]
-          res["pio_longitude"] = latlng[1]
-        end
-        if res["pio_startT"]
-          startT = Rational(res["pio_startT"], 1000)
-          res["pio_startT"] = Time.at(startT)
-        end
-        if res["pio_endT"]
-          endT = Rational(res["pio_endT"], 1000)
-          res["pio_endT"] = Time.at(endT)
-        end
-        res
-      else
-        begin
-          msg = response.body
-        rescue Exception
-          raise ItemNotFoundError, response
-        end
-        raise ItemNotFoundError, msg
-      end
-    end
-
-    # :category: Asynchronous Methods
-    # Asynchronously request to delete an item and return a PredictionIO::AsyncResponse object immediately.
-    #
-    # Corresponding REST API method: DELETE /items/:iid
-    #
-    # See also #delete_item.
-    def adelete_item(iid)
-      @http.adelete(PredictionIO::AsyncRequest.new("/items/#{iid}.#{@apiformat}",
-                                                   "pio_appkey" => @appkey,
-                                                   "pio_iid" => iid))
-    end
-
-    # :category: Synchronous Methods
-    # Synchronously request to delete an item and block until a response is received.
-    #
-    # See also #adelete_item.
-    #
-    # call-seq:
-    # delete_item(iid)
-    # delete_item(async_response)
-    def delete_item(iid_or_res)
-      if iid_or_res.is_a?(PredictionIO::AsyncResponse)
-        response = iid_or_res.get
-      else
-        response = adelete_item(iid_or_res).get
-      end
-      unless response.is_a?(Net::HTTPOK)
-        begin
-          msg = response.body
-        rescue Exception
-          raise ItemNotDeletedError, response
-        end
-        raise ItemNotDeletedError, msg
-      end
-    end
-
-    # Set the user ID for use in all subsequent user-to-item action recording and user recommendation retrieval.
-    def identify(uid)
-      @apiuid = uid
-    end
-
-    # :category: Asynchronous Methods
-    # Asynchronously request to get the top n recommendations for a user from an ItemRec engine and return a PredictionIO::AsyncResponse object immediately.
-    #
-    # Corresponding REST API method: GET /engines/itemrec/:engine/topn
-    #
-    # See also #get_itemrec_top_n.
-    def aget_itemrec_top_n(engine, n, params = {})
-      rparams = Hash.new
-      rparams["pio_appkey"] = @appkey
-      rparams["pio_uid"] = @apiuid
-      rparams["pio_n"] = n
-      if params["pio_itypes"]
-        if params["pio_itypes"].kind_of?(Array) && params["pio_itypes"].any?
-          rparams["pio_itypes"] = params["pio_itypes"].join(",")
-        else
-          rparams["pio_itypes"] = params["pio_itypes"]
-        end
-      end
-      if params["pio_latitude"] && params["pio_longitude"]
-        rparams["pio_latlng"] = "#{params["pio_latitude"]},#{params["pio_longitude"]}"
-      end
-      rparams["pio_within"] = params["pio_within"] if params["pio_within"]
-      rparams["pio_unit"] = params["pio_unit"] if params["pio_unit"]
-      if params["pio_attributes"]
-        if params["pio_attributes"].kind_of?(Array) && params["pio_attributes"].any?
-          rparams["pio_attributes"] = params["pio_attributes"].join(",")
-        else
-          rparams["pio_attributes"] = params["pio_attributes"]
-        end
-      end
-      @http.aget(PredictionIO::AsyncRequest.new("/engines/itemrec/#{engine}/topn.#{@apiformat}", rparams))
-    end
-
-    # :category: Synchronous Methods
-    # Synchronously request to get the top n recommendations for a user from an ItemRec engine and block until a response is received.
-    #
-    # See #aget_itemrec_top_n for a description of special argument handling.
-    #
-    # call-seq:
-    # get_itemrec_top_n(engine, n, params = {})
-    # get_itemrec_top_n(async_response)
-    def get_itemrec_top_n(*args)
-      uid_or_res = args[0]
-      if uid_or_res.is_a?(PredictionIO::AsyncResponse)
-        response = uid_or_res
-      else
-        response = aget_itemrec_top_n(*args)
-      end
-      http_response = response.get
-      if http_response.is_a?(Net::HTTPOK)
-        res = JSON.parse(http_response.body)
-        if response.request.params.has_key?('pio_attributes')
-          attributes = response.request.params['pio_attributes'].split(',')
-          list_of_attribute_values = attributes.map { |attrib| res[attrib] }
-          res["pio_iids"].zip(*list_of_attribute_values).map { |v| Hash[(['pio_iid'] + attributes).zip(v)] }
-        else
-          res["pio_iids"]
-        end
-      else
-        begin
-          msg = response.body
-        rescue Exception
-          raise ItemRecNotFoundError, response
-        end
-        raise ItemRecNotFoundError, msg
-      end
-    end
-
-    # :category: Asynchronous Methods
-    # Asynchronously request to get the ranking for a user from an ItemRank engine and return a PredictionIO::AsyncResponse object immediately.
-    #
-    # Corresponding REST API method: GET /engines/itemrank/:engine/ranked
-    #
-    # See also #get_itemrank_ranked.
-    def aget_itemrank_ranked(engine, iids, params = {})
-      rparams = Hash.new
-      rparams["pio_appkey"] = @appkey
-      rparams["pio_uid"] = @apiuid
-      if iids.kind_of?(Array) && iids.any?
-        rparams["pio_iids"] = iids.join(",")
-      else
-        rparams["pio_iids"] = iids
-      end
-      if params["pio_attributes"]
-        if params["pio_attributes"].kind_of?(Array) && params["pio_attributes"].any?
-          rparams["pio_attributes"] = params["pio_attributes"].join(",")
-        else
-          rparams["pio_attributes"] = params["pio_attributes"]
-        end
-      end
-      @http.aget(PredictionIO::AsyncRequest.new("/engines/itemrank/#{engine}/ranked.#{@apiformat}", rparams))
-    end
-
-    # :category: Synchronous Methods
-    # Synchronously request to get the ranking for a user from an ItemRank engine and block until a response is received.
-    #
-    # See #aget_itemrank_ranked for a description of special argument handling.
-    #
-    # call-seq:
-    # get_itemrank_ranked(engine, n, params = {})
-    # get_itemrank_ranked(async_response)
-    def get_itemrank_ranked(*args)
-      uid_or_res = args[0]
-      if uid_or_res.is_a?(PredictionIO::AsyncResponse)
-        response = uid_or_res
-      else
-        response = aget_itemrank_ranked(*args)
-      end
-      http_response = response.get
-      if http_response.is_a?(Net::HTTPOK)
-        res = JSON.parse(http_response.body)
-        if response.request.params.has_key?('pio_attributes')
-          attributes = response.request.params['pio_attributes'].split(',')
-          list_of_attribute_values = attributes.map { |attrib| res[attrib] }
-          res["pio_iids"].zip(*list_of_attribute_values).map { |v| Hash[(['pio_iid'] + attributes).zip(v)] }
-        else
-          res["pio_iids"]
-        end
-      else
-        begin
-          msg = response.body
-        rescue Exception
-          raise ItemRankNotFoundError, response
-        end
-        raise ItemRankNotFoundError, msg
-      end
-    end
-
-    # :category: Asynchronous Methods
-    # Asynchronously request to get the top n similar items for an item from an ItemSim engine and return a PredictionIO::AsyncResponse object immediately.
-    #
-    # Corresponding REST API method: GET /engines/itemsim/:engine/topn
-    #
-    # See also #get_itemsim_top_n.
-    def aget_itemsim_top_n(engine, iid, n, params = {})
-      rparams = Hash.new
-      rparams["pio_appkey"] = @appkey
-      rparams["pio_iid"] = iid
-      rparams["pio_n"] = n
-      if params["pio_itypes"]
-        if params["pio_itypes"].kind_of?(Array) && params["pio_itypes"].any?
-          rparams["pio_itypes"] = params["pio_itypes"].join(",")
-        else
-          rparams["pio_itypes"] = params["pio_itypes"]
-        end
-      end
-      if params["pio_latitude"] && params["pio_longitude"]
-        rparams["pio_latlng"] = "#{params["pio_latitude"]},#{params["pio_longitude"]}"
-      end
-      rparams["pio_within"] = params["pio_within"] if params["pio_within"]
-      rparams["pio_unit"] = params["pio_unit"] if params["pio_unit"]
-      if params["pio_attributes"]
-        if params["pio_attributes"].kind_of?(Array) && params["pio_attributes"].any?
-          rparams["pio_attributes"] = params["pio_attributes"].join(",")
-        else
-          rparams["pio_attributes"] = params["pio_attributes"]
-        end
-      end
-      @http.aget(PredictionIO::AsyncRequest.new("/engines/itemsim/#{engine}/topn.#{@apiformat}", rparams))
-    end
-
-    # :category: Synchronous Methods
-    # Synchronously request to get the top n similar items for an item from an ItemSim engine and block until a response is received.
-    #
-    # See #aget_itemsim_top_n for a description of special argument handling.
-    #
-    # call-seq:
-    # get_itemsim_top_n(engine, iid, n, params = {})
-    # get_itemsim_top_n(async_response)
-    def get_itemsim_top_n(*args)
-      uid_or_res = args[0]
-      if uid_or_res.is_a?(PredictionIO::AsyncResponse)
-        response = uid_or_res
-      else
-        response = aget_itemsim_top_n(*args)
-      end
-      http_response = response.get
-      if http_response.is_a?(Net::HTTPOK)
-        res = JSON.parse(http_response.body)
-        if response.request.params.has_key?('pio_attributes')
-          attributes = response.request.params['pio_attributes'].split(',')
-          list_of_attribute_values = attributes.map { |attrib| res[attrib] }
-          res["pio_iids"].zip(*list_of_attribute_values).map { |v| Hash[(['pio_iid'] + attributes).zip(v)] }
-        else
-          res["pio_iids"]
-        end
-      else
-        begin
-          msg = response.body
-        rescue Exception
-          raise ItemSimNotFoundError, response
-        end
-        raise ItemSimNotFoundError, msg
-      end
-    end
-
-    # :category: Asynchronous Methods
-    # Asynchronously request to record an action on an item and return a PredictionIO::AsyncResponse object immediately.
-    #
-    # Corresponding REST API method: POST /actions/u2i
-    #
-    # See also #record_action_on_item.
-    def arecord_action_on_item(action, iid, params = {})
-      rparams = params
-      rparams["pio_appkey"] = @appkey
-      rparams["pio_action"] = action
-      rparams["pio_uid"] = @apiuid
-      rparams["pio_iid"] = iid
-      rparams["pio_t"] = ((params["pio_t"].to_r) * 1000).round(0).to_s if params["pio_t"]
-      if params["pio_latitude"] && params["pio_longitude"]
-        rparams["pio_latlng"] = "#{params["pio_latitude"]},#{params["pio_longitude"]}"
-      end
-      @http.apost(PredictionIO::AsyncRequest.new("/actions/u2i.#{@apiformat}", rparams))
-    end
-
-    # :category: Synchronous Methods
-    # Synchronously request to record an action on an item and block until a response is received.
-    #
-    # See also #arecord_action_on_item.
-    #
-    # call-seq:
-    # record_action_on_item(action, iid, params = {})
-    # record_action_on_item(async_response)
-    def record_action_on_item(*args)
-      action_or_res = args[0]
-      if action_or_res.is_a?(PredictionIO::AsyncResponse)
-        response = action_or_res.get
-      else
-        response = arecord_action_on_item(*args).get
-      end
-      unless response.is_a?(Net::HTTPCreated)
-        begin
-          msg = response.body
-        rescue Exception
-          raise U2IActionNotCreatedError, response
-        end
-        raise U2IActionNotCreatedError, msg
-      end
-    end
-  end
-end
diff --git a/lib/predictionio/engine_client.rb b/lib/predictionio/engine_client.rb
index 021db1d..65ee8ad 100644
--- a/lib/predictionio/engine_client.rb
+++ b/lib/predictionio/engine_client.rb
@@ -49,7 +49,7 @@
 
     # Create a new PredictionIO Event Client with defaults:
     # - 1 concurrent HTTP(S) connections (threads)
-    # - API entry point at http://localhost:7070 (apiurl)
+    # - API entry point at http://localhost:8000 (apiurl)
     # - a 60-second timeout for each HTTP(S) connection (thread_timeout)
     def initialize(apiurl = 'http://localhost:8000', threads = 1,
                    thread_timeout = 60)
diff --git a/lib/predictionio/event_client.rb b/lib/predictionio/event_client.rb
index 7deaa53..83c73a5 100644
--- a/lib/predictionio/event_client.rb
+++ b/lib/predictionio/event_client.rb
@@ -41,7 +41,7 @@
   #     # Include the PredictionIO SDK
   #     require 'predictionio'
   #
-  #     client = PredictionIO::EventClient.new(<app_id>)
+  #     client = PredictionIO::EventClient.new(<access_key>)
   #
   # === Import a User Record from Your App (with asynchronous/non-blocking
   #     requests)
@@ -84,9 +84,9 @@
     # - 1 concurrent HTTP(S) connections (threads)
     # - API entry point at http://localhost:7070 (apiurl)
     # - a 60-second timeout for each HTTP(S) connection (thread_timeout)
-    def initialize(app_id, apiurl = 'http://localhost:7070', threads = 1,
+    def initialize(access_key, apiurl = 'http://localhost:7070', threads = 1,
                    thread_timeout = 60)
-      @app_id = app_id
+      @access_key = access_key
       @http = PredictionIO::Connection.new(URI(apiurl), threads, thread_timeout)
     end
 
@@ -135,11 +135,12 @@
     def acreate_event(event, entity_type, entity_id, optional = {})
       h = optional
       h.key?('eventTime') || h['eventTime'] = DateTime.now.to_s
-      h['appId'] = @app_id
       h['event'] = event
       h['entityType'] = entity_type
       h['entityId'] = entity_id
-      @http.apost(PredictionIO::AsyncRequest.new('/events.json', h.to_json))
+      @http.apost(PredictionIO::AsyncRequest.new(
+        "/events.json?accessKey=#{@access_key}", h.to_json
+      ))
     end
 
     # :category: Synchronous Methods
diff --git a/predictionio.gemspec b/predictionio.gemspec
index b9a2596..8bbb9f7 100644
--- a/predictionio.gemspec
+++ b/predictionio.gemspec
@@ -6,7 +6,7 @@
 provides convenient access of the PredictionIO API to Ruby programmers so that
 they can focus on their application logic.
   EOF
-  s.version = '0.8.0'
+  s.version = '0.8.2'
   s.licenses = ['Apache-2.0']
   s.author = 'The PredictionIO Team'
   s.email = 'support@prediction.io'
diff --git a/spec/predictionio_spec.rb b/spec/predictionio_spec.rb
index 911dc93..b7f2045 100644
--- a/spec/predictionio_spec.rb
+++ b/spec/predictionio_spec.rb
@@ -1,7 +1,6 @@
 require 'predictionio'
 require 'spec_helper'
 
-client = PredictionIO::Client.new('foobar', 10, 'http://fakeapi.com:8000')
 event_client = PredictionIO::EventClient.new(1, 'http://fakeapi.com:8000', 10)
 engine_client = PredictionIO::EngineClient.new('http://fakeapi.com:8000', 10)
 
@@ -50,92 +49,4 @@
       expect(predictions).to eq('iids' => %w(dead beef))
     end
   end
-
-  describe 'Users API' do
-    it 'create_user should create a user' do
-      response = client.create_user('foo')
-      expect(response).to_not raise_error
-    end
-    it 'get_user should get a user' do
-      response = client.get_user('foo')
-      expect(response).to eq('pio_uid' => 'foo')
-    end
-    it 'delete_user should delete a user' do
-      response = client.delete_user('foo')
-      expect(response).to_not raise_error
-    end
-  end
-
-  describe 'Items API' do
-    it 'create_item should create a item' do
-      response = client.create_item('bar', %w(dead beef))
-      expect(response).to_not raise_error
-    end
-    it 'get_item should get a item' do
-      response = client.get_item('bar')
-      expect(response).to eq('pio_iid' => 'bar',
-                             'pio_itypes' => %w(dead beef))
-    end
-    it 'delete_item should delete a item' do
-      response = client.delete_item('bar')
-      expect(response).to_not raise_error
-    end
-  end
-
-  describe 'U2I API' do
-    it 'record_action_on_item should record an action' do
-      client.identify('foo')
-      response = client.record_action_on_item('view', 'bar')
-      expect(response).to_not raise_error
-    end
-  end
-
-  describe 'Item Recommendation API' do
-    it 'provides recommendations to a user without attributes' do
-      client.identify('foo')
-      response = client.get_itemrec_top_n('itemrec-engine', 10)
-      expect(response).to eq(%w(x y z))
-    end
-    it 'provides recommendations to a user with attributes' do
-      client.identify('foo')
-      response = client.get_itemrec_top_n('itemrec-engine', 10,
-                                          'pio_attributes' => 'name')
-      expect(response).to eq([
-        { 'pio_iid' => 'x', 'name' => 'a' },
-        { 'pio_iid' => 'y', 'name' => 'b' },
-        { 'pio_iid' => 'z', 'name' => 'c' }])
-    end
-  end
-
-  describe 'Item Rank API' do
-    it 'provides ranking to a user without attributes' do
-      client.identify('foo')
-      response = client.get_itemrank_ranked('itemrank-engine', %w(y z x))
-      expect(response).to eq(%w(x y z))
-    end
-    it 'provides ranking to a user with attributes' do
-      client.identify('foo')
-      response = client.get_itemrank_ranked('itemrank-engine', %w(y x z),
-                                            'pio_attributes' => 'name')
-      expect(response).to eq([
-        { 'pio_iid' => 'x', 'name' => 'a' },
-        { 'pio_iid' => 'y', 'name' => 'b' },
-        { 'pio_iid' => 'z', 'name' => 'c' }])
-    end
-  end
-
-  describe 'Item Similarity API' do
-    it 'provides similarities to an item without attributes' do
-      response = client.get_itemsim_top_n('itemsim-engine', 'bar', 10)
-      expect(response).to eq(%w(x y z))
-    end
-    it 'provides similarities to an item with attributes' do
-      response = client.get_itemsim_top_n('itemsim-engine', 'bar', 10,
-                                          'pio_attributes' => 'name')
-      expect(response).to eq([
-        { 'pio_iid' => 'x', 'name' => 'a' },
-        { 'pio_iid' => 'y', 'name' => 'b' },
-        { 'pio_iid' => 'z', 'name' => 'c' }])
-    end
-  end
 end
diff --git a/spec/spec_helper.rb b/spec/spec_helper.rb
index 19d9d32..917ce65 100644
--- a/spec/spec_helper.rb
+++ b/spec/spec_helper.rb
@@ -8,41 +8,41 @@
 RSpec.configure do |config|
   config.before(:each) do
     # Events API
-    stub_request(:post, 'http://fakeapi.com:8000/events.json')
-      .with(body: hash_including(appId: 1, event: 'register',
+    stub_request(:post, 'http://fakeapi.com:8000/events.json?accessKey=1')
+      .with(body: hash_including(event: 'register',
                                  entityType: 'user', entityId: 'foobar'))
       .to_return(status: 201, body: JSON.generate(eventId: 'deadbeef00'))
-    stub_request(:post, 'http://fakeapi.com:8000/events.json')
-      .with(body: hash_including(appId: 1, event: '$set',
+    stub_request(:post, 'http://fakeapi.com:8000/events.json?accessKey=1')
+      .with(body: hash_including(event: '$set',
                                  entityType: 'pio_user', entityId: 'foobar'))
       .to_return(status: 201, body: JSON.generate(eventId: 'deadbeef01'))
-    stub_request(:post, 'http://fakeapi.com:8000/events.json')
-      .with(body: hash_including(appId: 1, event: '$unset',
+    stub_request(:post, 'http://fakeapi.com:8000/events.json?accessKey=1')
+      .with(body: hash_including(event: '$unset',
                                  entityType: 'pio_user', entityId: 'foobar',
                                  properties: { bar: 'baz' }))
       .to_return(status: 201, body: JSON.generate(eventId: 'deadbeef02'))
-    stub_request(:post, 'http://fakeapi.com:8000/events.json')
-      .with(body: hash_including(appId: 1, event: '$set',
+    stub_request(:post, 'http://fakeapi.com:8000/events.json?accessKey=1')
+      .with(body: hash_including(event: '$set',
                                  entityType: 'pio_item', entityId: 'foobar'))
       .to_return(status: 201, body: JSON.generate(eventId: 'deadbeef03'))
-    stub_request(:post, 'http://fakeapi.com:8000/events.json')
-      .with(body: hash_including(appId: 1, event: '$unset',
+    stub_request(:post, 'http://fakeapi.com:8000/events.json?accessKey=1')
+      .with(body: hash_including(event: '$unset',
                                  entityType: 'pio_item', entityId: 'foobar',
                                  properties: { bar: 'baz' }))
       .to_return(status: 201, body: JSON.generate(eventId: 'deadbeef04'))
-    stub_request(:post, 'http://fakeapi.com:8000/events.json')
-      .with(body: hash_including(appId: 1, event: 'greet',
+    stub_request(:post, 'http://fakeapi.com:8000/events.json?accessKey=1')
+      .with(body: hash_including(event: 'greet',
                                  entityType: 'pio_user', entityId: 'foobar',
                                  targetEntityType: 'pio_item',
                                  targetEntityId: 'barbaz',
                                  properties: { dead: 'beef' }))
       .to_return(status: 201, body: JSON.generate(eventId: 'deadbeef05'))
-    stub_request(:post, 'http://fakeapi.com:8000/events.json')
-      .with(body: hash_including(appId: 1, event: '$delete',
+    stub_request(:post, 'http://fakeapi.com:8000/events.json?accessKey=1')
+      .with(body: hash_including(event: '$delete',
                                  entityType: 'pio_user', entityId: 'foobar'))
       .to_return(status: 201, body: JSON.generate(eventId: 'deadbeef06'))
-    stub_request(:post, 'http://fakeapi.com:8000/events.json')
-      .with(body: hash_including(appId: 1, event: '$delete',
+    stub_request(:post, 'http://fakeapi.com:8000/events.json?accessKey=1')
+      .with(body: hash_including(event: '$delete',
                                  entityType: 'pio_item', entityId: 'foobar'))
       .to_return(status: 201, body: JSON.generate(eventId: 'deadbeef07'))
 
@@ -51,92 +51,5 @@
       .with(body: { uid: 'foobar' })
       .to_return(status: 200, body: JSON.generate(iids: %w(dead beef)),
                  headers: {})
-
-    # Users API
-    stub_request(:post, 'http://fakeapi.com:8000/users.json')
-      .with(body: { pio_appkey: 'foobar', pio_uid: 'foo' })
-      .to_return(status: 201, body: '', headers: {})
-    stub_request(:get, 'http://fakeapi.com:8000/users/foo.json')
-      .with(query: hash_including(pio_appkey: 'foobar'))
-      .to_return(status: 200, body: JSON.generate(pio_uid: 'foo'), headers: {})
-    stub_request(:delete, 'http://fakeapi.com:8000/users/foo.json')
-      .with(query: hash_including(pio_appkey: 'foobar'))
-      .to_return(status: 200, body: '', headers: {})
-
-    # Items API
-    stub_request(:post, 'http://fakeapi.com:8000/items.json')
-      .with(body: { pio_appkey: 'foobar', pio_iid: 'bar',
-                    pio_itypes: 'dead,beef' })
-      .to_return(status: 201, body: '', headers: {})
-    stub_request(:get, 'http://fakeapi.com:8000/items/bar.json')
-      .with(query: hash_including(pio_appkey: 'foobar'))
-      .to_return(status: 200,
-                 body: JSON.generate(pio_iid: 'bar', pio_itypes: %w(dead beef)),
-                 headers: {})
-    stub_request(:delete, 'http://fakeapi.com:8000/items/bar.json')
-      .with(query: hash_including(pio_appkey: 'foobar'))
-      .to_return(status: 200, body: '', headers: {})
-
-    # U2I Actions API
-    stub_request(:post, 'http://fakeapi.com:8000/actions/u2i.json')
-      .with(body: { pio_action: 'view', pio_appkey: 'foobar', pio_iid: 'bar',
-                    pio_uid: 'foo' })
-      .to_return(status: 201, body: '', headers: {})
-
-    # Item Recommendation API
-    stub_request(
-      :get,
-      'http://fakeapi.com:8000/engines/itemrec/itemrec-engine/topn.json')
-      .with(query: hash_including(pio_appkey: 'foobar', pio_n: '10',
-                                  pio_uid: 'foo'))
-      .to_return(status: 200, body: JSON.generate(pio_iids: %w(x y z)),
-                 headers: {})
-
-    stub_request(
-      :get,
-      'http://fakeapi.com:8000/engines/itemrec/itemrec-engine/topn.json')
-      .with(query: hash_including(pio_appkey: 'foobar', pio_n: '10',
-                                  pio_uid: 'foo', pio_attributes: 'name'))
-      .to_return(status: 200,
-                 body: JSON.generate(pio_iids: %w(x y z), name: %w(a b c)),
-                 headers: {})
-
-    # Item Recommendation API
-    stub_request(
-      :get,
-      'http://fakeapi.com:8000/engines/itemrank/itemrank-engine/ranked.json')
-      .with(query: hash_including(pio_appkey: 'foobar', pio_iids: 'y,z,x',
-                                  pio_uid: 'foo'))
-      .to_return(status: 200, body: JSON.generate(pio_iids: %w(x y z)),
-                 headers: {})
-
-    stub_request(
-      :get,
-      'http://fakeapi.com:8000/engines/itemrank/itemrank-engine/ranked.json')
-      .with(query: hash_including(pio_appkey: 'foobar', pio_iids: 'y,x,z',
-                                  pio_uid: 'foo', pio_attributes: 'name'))
-      .to_return(status: 200,
-                 body: JSON.generate(pio_iids: %w(x y z), name: %w(a b c)),
-                 headers: {})
-
-    # Item Similarity API
-    stub_request(
-      :get,
-      'http://fakeapi.com:8000/engines/itemsim/itemsim-engine/topn.json')
-      .with(query: hash_including(pio_appkey: 'foobar', pio_n: '10',
-                                  pio_iid: 'bar'))
-      .to_return(status: 200,
-                 body: JSON.generate(pio_iids: %w(x y z)),
-                 headers: {})
-
-    stub_request(
-      :get,
-      'http://fakeapi.com:8000/engines/itemsim/itemsim-engine/topn.json')
-      .with(query: hash_including(pio_appkey: 'foobar', pio_n: '10',
-                                  pio_iid: 'bar', pio_attributes: 'name'))
-      .to_return(status: 200,
-                 body: JSON.generate('pio_iids' => %w(x y z),
-                                     'name' => %w(a b c)),
-                 headers: {})
   end
 end