Merge branch 'develop'
diff --git a/Gemfile b/Gemfile
index 32c3ae0..783e457 100644
--- a/Gemfile
+++ b/Gemfile
@@ -1,4 +1,4 @@
-source "https://rubygems.org"
+source 'https://rubygems.org'
 group :test do
   gem 'coveralls', require: false
   gem 'rspec', '~> 2.14.1'
diff --git a/Rakefile b/Rakefile
index 70a846d..f34a48c 100644
--- a/Rakefile
+++ b/Rakefile
@@ -2,4 +2,4 @@
 
 RSpec::Core::RakeTask.new(:spec)
 
-task :default => :spec
+task default: :spec
diff --git a/lib/predictionio.rb b/lib/predictionio.rb
index 235c10c..73250bd 100644
--- a/lib/predictionio.rb
+++ b/lib/predictionio.rb
@@ -1 +1,38 @@
-require "predictionio/client"
+require 'predictionio/client'
+require 'predictionio/event_client'
+require 'predictionio/engine_client'
+
+# The PredictionIO module contains classes that provide convenient access of
+# PredictionIO Event API and Engine Instance over HTTP/HTTPS.
+#
+# To create an app and perform predictions, please download the PredictionIO
+# suite from http://prediction.io.
+#
+# Most functionality is provided by PredictionIO::EventClient and
+# PredictionIO::EngineClient classes.
+#
+# == Deprecation Notice
+#
+# Pre-0.7.x series support is now deprecated. All existing users are strongly
+# encouraged to migrate to 0.8.x.
+#
+# The old Client interface is retained in case of any accidental Gem upgrade.
+# It will be removed in the next minor version.
+#
+# == 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.
+#
+# == Installation
+# The easiest way is to use RubyGems:
+#     gem install predictionio
+module PredictionIO
+end
diff --git a/lib/predictionio/async_request.rb b/lib/predictionio/async_request.rb
index ea89416..a2c1b5e 100644
--- a/lib/predictionio/async_request.rb
+++ b/lib/predictionio/async_request.rb
@@ -1,5 +1,6 @@
 module PredictionIO
-  # This class contains the URI path and query parameters that is consumed by PredictionIO::Connection for asynchronous HTTP requests.
+  # This class contains the URI path and query parameters that is consumed by
+  # PredictionIO::Connection for asynchronous HTTP requests.
   class AsyncRequest
 
     # The path portion of the request URI.
@@ -8,7 +9,8 @@
     # Query parameters, or form data.
     attr_reader :params
 
-    # Populates the package with request URI path, and optionally query parameters or form data.
+    # Populates the package with request URI path, and optionally query
+    # parameters or form data.
     def initialize(path, params = {})
       @params = params
       @path = path
diff --git a/lib/predictionio/client.rb b/lib/predictionio/client.rb
index 4ef2992..e2b405b 100644
--- a/lib/predictionio/client.rb
+++ b/lib/predictionio/client.rb
@@ -11,13 +11,7 @@
 require 'predictionio/async_response'
 require 'predictionio/connection'
 
-# The PredictionIO module contains classes that provide convenient access of the PredictionIO output API over HTTP/HTTPS.
-#
-# To create an app and perform predictions, please download the PredictionIO suite from http://prediction.io.
-#
-# Most functionality is provided by the PredictionIO::Client class.
 module PredictionIO
-
   # This class contains methods that access PredictionIO via REST requests.
   #
   # Many REST request methods support optional arguments.
@@ -505,8 +499,8 @@
     # See #aget_itemrec_top_n for a description of special argument handling.
     #
     # call-seq:
-    # aget_itemrec_top_n(engine, n, params = {})
-    # aget_itemrec_top_n(async_response)
+    # 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)
@@ -565,8 +559,8 @@
     # See #aget_itemrank_ranked for a description of special argument handling.
     #
     # call-seq:
-    # aget_itemrank_ranked(engine, n, params = {})
-    # aget_itemrank_ranked(async_response)
+    # 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)
@@ -633,8 +627,8 @@
     # See #aget_itemsim_top_n for a description of special argument handling.
     #
     # call-seq:
-    # aget_itemsim_top_n(engine, iid, n, params = {})
-    # aget_itemsim_top_n(async_response)
+    # 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)
diff --git a/lib/predictionio/connection.rb b/lib/predictionio/connection.rb
index d9bdc28..2644bb9 100644
--- a/lib/predictionio/connection.rb
+++ b/lib/predictionio/connection.rb
@@ -34,29 +34,34 @@
                   request = package[:request]
                   response = package[:response]
                   case package[:method]
-                  when "get"
+                  when 'get'
                     http_req = Net::HTTP::Get.new("#{uri.path}#{request.qpath}")
                     begin
                       response.set(http.request(http_req))
                     rescue Exception => details
                       response.set(details)
                     end
-                  when "post"
-                    http_req = Net::HTTP::Post.new("#{uri.path}#{request.path}")
-                    http_req.set_form_data(request.params)
+                  when 'post'
+                    if request.params.is_a?(Hash)
+                      http_req = Net::HTTP::Post.new("#{uri.path}#{request.path}")
+                      http_req.set_form_data(request.params)
+                    else
+                      http_req = Net::HTTP::Post.new("#{uri.path}#{request.path}", initheader = {'Content-Type' => 'application/json'})
+                      http_req.body = request.params
+                    end
                     begin
                       response.set(http.request(http_req))
                     rescue Exception => details
                       response.set(details)
                     end
-                  when "delete"
+                  when 'delete'
                     http_req = Net::HTTP::Delete.new("#{uri.path}#{request.qpath}")
                     begin
                       response.set(http.request(http_req))
                     rescue Exception => details
                       response.set(details)
                     end
-                  when "exit"
+                  when 'exit'
                     @counter_lock.synchronize do
                       @connections -= 1
                     end
@@ -96,17 +101,17 @@
 
     # Shortcut to create an asynchronous GET request with the response object returned.
     def aget(areq)
-      request("get", areq)
+      request('get', areq)
     end
 
     # Shortcut to create an asynchronous POST request with the response object returned.
     def apost(areq)
-      request("post", areq)
+      request('post', areq)
     end
 
     # Shortcut to create an asynchronous DELETE request with the response object returned.
     def adelete(areq)
-      request("delete", areq)
+      request('delete', areq)
     end
   end
 end
diff --git a/lib/predictionio/engine_client.rb b/lib/predictionio/engine_client.rb
new file mode 100644
index 0000000..021db1d
--- /dev/null
+++ b/lib/predictionio/engine_client.rb
@@ -0,0 +1,127 @@
+# Ruby SDK for convenient access of PredictionIO Output API.
+#
+# Author::    PredictionIO Team (support@prediction.io)
+# Copyright:: Copyright (c) 2014 TappingStone, Inc.
+# License::   Apache License, Version 2.0
+
+require 'predictionio/async_request'
+require 'predictionio/async_response'
+require 'predictionio/connection'
+
+module PredictionIO
+  # This class contains methods that interface with PredictionIO Engine
+  # Instances that are trained from PredictionIO built-in Engines.
+  #
+  # 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.
+  #
+  # == Synopsis
+  # In most cases, using synchronous methods. If you have a special performance
+  # requirement, you may want to take a look at asynchronous methods.
+  #
+  # === Instantiate an EngineClient
+  #     # Include the PredictionIO SDK
+  #     require 'predictionio'
+  #
+  #     client = PredictionIO::EngineClient.new
+  #
+  # === Send a Query to Retrieve Predictions
+  #     # PredictionIO call to record the view action
+  #     begin
+  #       result = client.query('uid' => 'foobar')
+  #     rescue NotFoundError => e
+  #       ...
+  #     rescue BadRequestError => e
+  #       ...
+  #     rescue ServerError => e
+  #       ...
+  #     end
+  class EngineClient
+    # Raised when an event is not created after a synchronous API call.
+    class NotFoundError < StandardError; end
+
+    # Raised when the query is malformed.
+    class BadRequestError < StandardError; end
+
+    # Raised when the Engine Instance returns a server error.
+    class ServerError < StandardError; end
+
+    # Create a new PredictionIO Event Client with defaults:
+    # - 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(apiurl = 'http://localhost:8000', threads = 1,
+                   thread_timeout = 60)
+      @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
+        status
+      end
+    end
+
+    protected
+
+    # Internal helper method. Do not call directly.
+    def sync_events(sync_m, *args)
+      if args[0].is_a?(PredictionIO::AsyncResponse)
+        response = args[0].get
+      else
+        response = send(sync_m, *args).get
+      end
+      return JSON.parse(response.body) if response.is_a?(Net::HTTPOK)
+      begin
+        msg = response.body
+      rescue
+        raise response
+      end
+      if response.is_a?(Net::HTTPBadRequest)
+        fail BadRequestError, msg
+      elsif response.is_a?(Net::HTTPNotFound)
+        fail NotFoundError, msg
+      elsif response.is_a?(Net::HTTPServerError)
+        fail ServerError, msg
+      else
+        fail msg
+      end
+    end
+
+    public
+
+    # :category: Asynchronous Methods
+    # Asynchronously sends a query and returns PredictionIO::AsyncResponse
+    # object immediately. The query should be a Ruby data structure that can be
+    # converted to a JSON object.
+    #
+    # Corresponding REST API method: POST /
+    #
+    # See also #send_query.
+    def asend_query(query)
+      @http.apost(PredictionIO::AsyncRequest.new('/queries.json',
+                                                 query.to_json))
+    end
+
+    # :category: Synchronous Methods
+    # Synchronously sends a query and block until predictions are received.
+    #
+    # See also #asend_query.
+    #
+    # call-seq:
+    # send_query(data)
+    # send_query(async_response)
+    def send_query(*args)
+      sync_events(:asend_query, *args)
+    end
+  end
+end
diff --git a/lib/predictionio/event_client.rb b/lib/predictionio/event_client.rb
new file mode 100644
index 0000000..7deaa53
--- /dev/null
+++ b/lib/predictionio/event_client.rb
@@ -0,0 +1,340 @@
+# Ruby SDK for convenient access of PredictionIO Output API.
+#
+# Author::    PredictionIO Team (support@prediction.io)
+# Copyright:: Copyright (c) 2014 TappingStone, Inc.
+# License::   Apache License, Version 2.0
+
+require 'predictionio/async_request'
+require 'predictionio/async_response'
+require 'predictionio/connection'
+require 'date'
+
+module PredictionIO
+  # This class contains methods that interface with the PredictionIO Event
+  # Server via the PredictionIO Event API using 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.
+  #
+  # == Installation
+  # The easiest way is to use RubyGems:
+  #     gem install predictionio
+  #
+  # == Synopsis
+  # In most cases, using synchronous methods. If you have a special performance
+  # requirement, you may want to take a look at asynchronous methods.
+  #
+  # === Instantiate an EventClient
+  #     # Include the PredictionIO SDK
+  #     require 'predictionio'
+  #
+  #     client = PredictionIO::EventClient.new(<app_id>)
+  #
+  # === 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.aset_user(uid)
+  #
+  #     #
+  #     # (other work to do for the rest of the page)
+  #     #
+  #
+  #     begin
+  #       # PredictionIO call to retrieve results from an asynchronous response
+  #       result = client.set_user(response)
+  #     rescue PredictionIO::EventClient::NotCreatedError => 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
+  #       result = client.record_user_action_on_item('rate', 'foouser',
+  #                                                  'baritem',
+  #                                                  'pio_rating' => 4)
+  #     rescue PredictionIO::EventClient::NotCreatedError => e
+  #       ...
+  #     end
+  class EventClient
+    # Raised when an event is not created after a synchronous API call.
+    class NotCreatedError < StandardError; end
+
+    # Create a new PredictionIO Event Client with defaults:
+    # - 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,
+                   thread_timeout = 60)
+      @app_id = app_id
+      @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
+        status
+      end
+    end
+
+    protected
+
+    # Internal helper method. Do not call directly.
+    def sync_events(sync_m, *args)
+      if args[0].is_a?(PredictionIO::AsyncResponse)
+        response = args[0].get
+      else
+        response = send(sync_m, *args).get
+      end
+      return response if response.is_a?(Net::HTTPCreated)
+      begin
+        msg = response.body
+      rescue
+        raise NotCreatedError, response
+      end
+      fail NotCreatedError, msg
+    end
+
+    public
+
+    # :category: Asynchronous Methods
+    # Asynchronously request to create an event and return a
+    # PredictionIO::AsyncResponse object immediately.
+    #
+    # Corresponding REST API method: POST /events.json
+    #
+    # See also #create_event.
+    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))
+    end
+
+    # :category: Synchronous Methods
+    # Synchronously request to create an event and block until a response is
+    # received.
+    #
+    # See also #acreate_event.
+    #
+    # call-seq:
+    # create_event(event, entity_type, entity_id, optional = {})
+    # create_event(async_response)
+    def create_event(*args)
+      sync_events(:acreate_event, *args)
+    end
+
+    # :category: Asynchronous Methods
+    # Asynchronously request to set properties of a user and return a
+    # PredictionIO::AsyncResponse object immediately.
+    #
+    # Corresponding REST API method: POST /events.json
+    #
+    # See also #set_user.
+    def aset_user(uid, optional = {})
+      acreate_event('$set', 'pio_user', uid, optional)
+    end
+
+    # :category: Synchronous Methods
+    # Synchronously request to set properties of a user and block until a
+    # response is received.
+    #
+    # See also #aset_user.
+    #
+    # call-seq:
+    # set_user(uid, optional = {})
+    # set_user(async_response)
+    def set_user(*args)
+      sync_events(:aset_user, *args)
+    end
+
+    # :category: Asynchronous Methods
+    # Asynchronously request to unset properties of a user and return a
+    # PredictionIO::AsyncResponse object immediately.
+    #
+    # properties must be a non-empty Hash.
+    #
+    # Corresponding REST API method: POST /events.json
+    #
+    # See also #unset_user.
+    def aunset_user(uid, optional)
+      optional.key?('properties') ||
+        fail(ArgumentError, 'properties must be present when event is $unset')
+      optional['properties'].empty? &&
+        fail(ArgumentError, 'properties cannot be empty when event is $unset')
+      acreate_event('$unset', 'pio_user', uid, optional)
+    end
+
+    # :category: Synchronous Methods
+    # Synchronously request to unset properties of a user and block until a
+    # response is received.
+    #
+    # See also #aunset_user.
+    #
+    # call-seq:
+    # unset_user(uid, optional)
+    # unset_user(async_response)
+    def unset_user(*args)
+      sync_events(:aunset_user, *args)
+    end
+
+    # :category: Asynchronous Methods
+    # Asynchronously request to delete a user and return a
+    # PredictionIO::AsyncResponse object immediately.
+    #
+    # Corresponding REST API method: POST /events.json
+    #
+    # See also #delete_user.
+    def adelete_user(uid)
+      acreate_event('$delete', 'pio_user', 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(*args)
+      sync_events(:adelete_user, *args)
+    end
+
+    # :category: Asynchronous Methods
+    # Asynchronously request to set properties of an item and return a
+    # PredictionIO::AsyncResponse object immediately.
+    #
+    # Corresponding REST API method: POST /events.json
+    #
+    # See also #set_item.
+    def aset_item(iid, optional = {})
+      acreate_event('$set', 'pio_item', iid, optional)
+    end
+
+    # :category: Synchronous Methods
+    # Synchronously request to set properties of an item and block until a
+    # response is received.
+    #
+    # See also #aset_item.
+    #
+    # call-seq:
+    # set_item(iid, properties = {}, optional = {})
+    # set_item(async_response)
+    def set_item(*args)
+      sync_events(:aset_item, *args)
+    end
+
+    # :category: Asynchronous Methods
+    # Asynchronously request to unset properties of an item and return a
+    # PredictionIO::AsyncResponse object immediately.
+    #
+    # properties must be a non-empty Hash.
+    #
+    # Corresponding REST API method: POST /events.json
+    #
+    # See also #unset_item.
+    def aunset_item(iid, optional)
+      optional.key?('properties') ||
+        fail(ArgumentError, 'properties must be present when event is $unset')
+      optional['properties'].empty? &&
+        fail(ArgumentError, 'properties cannot be empty when event is $unset')
+      acreate_event('$unset', 'pio_item', iid, optional)
+    end
+
+    # :category: Synchronous Methods
+    # Synchronously request to unset properties of an item and block until a
+    # response is received.
+    #
+    # See also #aunset_item.
+    #
+    # call-seq:
+    # unset_item(iid, properties, optional = {})
+    # unset_item(async_response)
+    def unset_item(*args)
+      sync_events(:aunset_item, *args)
+    end
+
+    # :category: Asynchronous Methods
+    # Asynchronously request to delete an item and return a
+    # PredictionIO::AsyncResponse object immediately.
+    #
+    # Corresponding REST API method: POST /events.json
+    #
+    # See also #delete_item.
+    def adelete_item(uid)
+      acreate_event('$delete', 'pio_item', uid)
+    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(uid)
+    # delete_item(async_response)
+    def delete_item(*args)
+      sync_events(:adelete_item, *args)
+    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 /events.json
+    #
+    # See also #record_user_action_on_item.
+    def arecord_user_action_on_item(action, uid, iid, optional = {})
+      optional['targetEntityType'] = 'pio_item'
+      optional['targetEntityId'] = iid
+      acreate_event(action, 'pio_user', uid, optional)
+    end
+
+    # :category: Synchronous Methods
+    # Synchronously request to record an action on an item and block until a
+    # response is received.
+    #
+    # See also #arecord_user_action_on_item.
+    #
+    # call-seq:
+    # record_user_action_on_item(action, uid, iid, optional = {})
+    # record_user_action_on_item(async_response)
+    def record_user_action_on_item(*args)
+      sync_events(:arecord_user_action_on_item, *args)
+    end
+  end
+end
diff --git a/predictionio.gemspec b/predictionio.gemspec
index 5c612e4..b9a2596 100644
--- a/predictionio.gemspec
+++ b/predictionio.gemspec
@@ -1,15 +1,16 @@
 Gem::Specification.new do |s|
-  s.name = "predictionio"
-  s.summary = "PredictionIO Ruby SDK"
+  s.name = 'predictionio'
+  s.summary = 'PredictionIO Ruby SDK'
   s.description = <<-EOF
 PredictionIO is a prediction server for building smart applications. This gem
 provides convenient access of the PredictionIO API to Ruby programmers so that
 they can focus on their application logic.
   EOF
-  s.version = "0.7.1"
-  s.author = "The PredictionIO Team"
-  s.email = "support@prediction.io"
-  s.homepage = "http://prediction.io"
+  s.version = '0.8.0'
+  s.licenses = ['Apache-2.0']
+  s.author = 'The PredictionIO Team'
+  s.email = 'support@prediction.io'
+  s.homepage = 'http://prediction.io'
   s.platform = Gem::Platform::RUBY
   s.required_ruby_version = '>=1.9'
   s.files = Dir[File.join('lib', '**', '**')]
diff --git a/spec/predictionio_spec.rb b/spec/predictionio_spec.rb
index 0e75ad6..911dc93 100644
--- a/spec/predictionio_spec.rb
+++ b/spec/predictionio_spec.rb
@@ -1,90 +1,141 @@
 require 'predictionio'
 require 'spec_helper'
 
-client = PredictionIO::Client.new("foobar", 10, "http://fakeapi.com:8000")
+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)
 
 describe PredictionIO do
+  describe 'Events API' do
+    it 'create_event should create an event' do
+      response = event_client.create_event('register', 'user', 'foobar')
+      expect(response).to_not raise_error
+    end
+    it 'set_user should set user properties' do
+      response = event_client.set_user('foobar')
+      expect(response).to_not raise_error
+    end
+    it 'unset_user should unset user properties' do
+      response = event_client.unset_user('foobar',
+                                         'properties' => { 'bar' => 'baz' })
+      expect(response).to_not raise_error
+    end
+    it 'set_item should set item properties' do
+      response = event_client.set_item('foobar')
+      expect(response).to_not raise_error
+    end
+    it 'unset_item should unset item properties' do
+      response = event_client.unset_item('foobar',
+                                         'properties' => { 'bar' => 'baz' })
+      expect(response).to_not raise_error
+    end
+    it 'record_user_action_on_item should record a U2I action' do
+      response = event_client.record_user_action_on_item(
+        'greet', 'foobar', 'barbaz', 'properties' => { 'dead' => 'beef' })
+      expect(response).to_not raise_error
+    end
+    it 'delete_user should delete a user' do
+      response = event_client.delete_user('foobar')
+      expect(response).to_not raise_error
+    end
+    it 'delete_item should delete an item' do
+      response = event_client.delete_item('foobar')
+      expect(response).to_not raise_error
+    end
+  end
+
+  describe 'Engine Client' do
+    it 'send_query should get predictions' do
+      predictions = engine_client.send_query('uid' => 'foobar')
+      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")
+      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"})
+      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")
+      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", ["dead", "beef"])
+      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" => ["dead", "beef"]})
+      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")
+      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")
+      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(["x", "y", "z"])
+      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')
+      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"}])
+        { '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", ["y", "z", "x"])
-      expect(response).to eq(["x", "y", "z"])
+      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", ["y", "x", "z"], 'pio_attributes' => 'name')
+      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"}])
+        { '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(["x", "y", "z"])
+      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')
+      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"}])
+        { '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 2cc038a..19d9d32 100644
--- a/spec/spec_helper.rb
+++ b/spec/spec_helper.rb
@@ -7,58 +7,136 @@
 
 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',
+                                 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',
+                                 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',
+                                 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',
+                                 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',
+                                 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',
+                                 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',
+                                 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',
+                                 entityType: 'pio_item', entityId: 'foobar'))
+      .to_return(status: 201, body: JSON.generate(eventId: 'deadbeef07'))
+
+    # Engine Instance
+    stub_request(:post, 'http://fakeapi.com:8000/queries.json')
+      .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 => {})
+    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" => ["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 => {})
+    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 => {})
+    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" => ["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'))
+      .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" => ["x", "y", "z"], "name" => ["a", "b", "c"]}), :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" => ["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,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" => ["x", "y", "z"], "name" => ["a", "b", "c"]}), :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" => ["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'))
+      .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" => ["x", "y", "z"], 'name' => ['a', 'b', 'c']}), :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