Skip to content

Commit

Permalink
Merge pull request #42 from customerio/net-http
Browse files Browse the repository at this point in the history
Replace HTTParty with net/http
  • Loading branch information
alisdair committed Mar 15, 2016
2 parents f505167 + 5db6a7f commit 743d545
Show file tree
Hide file tree
Showing 6 changed files with 149 additions and 30 deletions.
2 changes: 0 additions & 2 deletions .travis.yml
Expand Up @@ -12,5 +12,3 @@ rvm:
- jruby-head
- ree
- ruby-head
- rbx-18mode
- rbx-19mode
1 change: 0 additions & 1 deletion customerio.gemspec
Expand Up @@ -15,7 +15,6 @@ Gem::Specification.new do |gem|
gem.require_paths = ["lib"]
gem.version = Customerio::VERSION

gem.add_dependency('httparty', ["< 0.12", ">= 0.5"])
gem.add_dependency('multi_json', "~> 1.0")

gem.add_development_dependency('rake')
Expand Down
1 change: 1 addition & 0 deletions lib/customerio.rb
Expand Up @@ -2,4 +2,5 @@

module Customerio
require "customerio/client"
require "customerio/param_encoder"
end
73 changes: 53 additions & 20 deletions lib/customerio/client.rb
@@ -1,13 +1,13 @@
require 'httparty'
require 'net/http'
require 'multi_json'

module Customerio
class Client
include HTTParty
base_uri 'https://track.customer.io'
default_timeout 10
DEFAULT_BASE_URI = 'https://track.customer.io'
DEFAULT_TIMEOUT = 10

class Client
class MissingIdAttributeError < RuntimeError; end
class InvalidRequest < RuntimeError; end
class InvalidResponse < RuntimeError
attr_reader :response

Expand All @@ -18,16 +18,19 @@ def initialize(message, response)
end

def initialize(site_id, secret_key, options = {})
@auth = { :username => site_id, :password => secret_key }
@username = site_id
@password = secret_key
@json = options.has_key?(:json) ? options[:json] : true
@base_uri = options[:base_uri] || DEFAULT_BASE_URI
@timeout = options[:timeout] || DEFAULT_TIMEOUT
end

def identify(attributes)
create_or_update(attributes)
end

def delete(customer_id)
verify_response(self.class.delete(customer_path(customer_id), options))
verify_response(request(:delete, customer_path(customer_id)))
end

def track(*args)
Expand Down Expand Up @@ -59,11 +62,7 @@ def create_or_update(attributes = {})

url = customer_path(attributes[:id])

if @json
verify_response(self.class.put(url, options.merge(:body => MultiJson.dump(attributes), :headers => {'Content-Type' => 'application/json'})))
else
verify_response(self.class.put(url, options.merge(:body => attributes, :headers => {'Content-Type' => 'application/x-www-form-urlencoded'})))
end
verify_response(request(:put, url, attributes))
end

def create_customer_event(customer_id, event_name, attributes = {})
Expand All @@ -77,11 +76,7 @@ def create_anonymous_event(event_name, attributes = {})
def create_event(url, event_name, attributes = {})
body = { :name => event_name, :data => attributes }
body[:timestamp] = attributes[:timestamp] if valid_timestamp?(attributes[:timestamp])
if @json
verify_response(self.class.post(url, options.merge(:body => MultiJson.dump(body), :headers => {'Content-Type' => 'application/json'})))
else
verify_response(self.class.post(url, options.merge(:body => body, :headers => {'Content-Type' => 'application/x-www-form-urlencoded'})))
end
verify_response(request(:post, url, body))
end

def customer_path(id)
Expand All @@ -94,7 +89,7 @@ def valid_timestamp?(timestamp)


def verify_response(response)
if response.code >= 200 && response.code < 300
if response.code.to_i >= 200 && response.code.to_i < 300
response
else
raise InvalidResponse.new("Customer.io API returned an invalid response: #{response.code}", response)
Expand All @@ -106,8 +101,46 @@ def extract_attributes(args)
hash.inject({}){ |hash, (k,v)| hash[k.to_sym] = v; hash }
end

def options
{ :basic_auth => @auth }
def request(method, path, body = nil, headers = {})
uri = URI.join(@base_uri, path)

session = Net::HTTP.new(uri.host, uri.port)
session.use_ssl = (uri.scheme == 'https')
session.open_timeout = @timeout
session.read_timeout = @timeout

req = request_class(method).new(uri.path)
req.initialize_http_header(headers)
req.basic_auth @username, @password

add_request_body(req, body) unless body.nil?

session.start do |http|
http.request(req)
end
end

def request_class(method)
case method
when :post
Net::HTTP::Post
when :put
Net::HTTP::Put
when :delete
Net::HTTP::Delete
else
raise InvalidRequest.new("Invalid request method #{method.inspect}")
end
end

def add_request_body(req, body)
if @json
req.add_field('Content-Type', 'application/json')
req.body = MultiJson.dump(body)
else
req.add_field('Content-Type', 'application/x-www-form-urlencoded')
req.body = ParamEncoder.to_params(body)
end
end
end
end
68 changes: 68 additions & 0 deletions lib/customerio/param_encoder.rb
@@ -0,0 +1,68 @@
# Based on HTTParty's HashConversions:
#
# https://github.com/jnunemaker/httparty/blob/master/lib/httparty/hash_conversions.rb
#
# License: MIT https://github.com/jnunemaker/httparty/blob/master/MIT-LICENSE

require 'erb'
require 'uri'

module Customerio
class ParamEncoder
# @return <String> This hash as a query string
#
# @example
# { name: "Bob",
# address: {
# street: '111 Ruby Ave.',
# city: 'Ruby Central',
# phones: ['111-111-1111', '222-222-2222']
# }
# }.to_params
# #=> "name=Bob&address[city]=Ruby Central&address[phones][]=111-111-1111&address[phones][]=222-222-2222&address[street]=111 Ruby Ave."
def self.to_params(hash)
hash.to_hash.map { |k, v| normalize_param(k, v) }.join.chop
end

# @param key<Object> The key for the param.
# @param value<Object> The value for the param.
#
# @return <String> This key value pair as a param
#
# @example normalize_param(:name, "Bob Jones") #=> "name=Bob%20Jones&"
def self.normalize_param(key, value)
param = ''
stack = []

if value.respond_to?(:to_ary)
param << value.to_ary.map { |element| normalize_param("#{key}[]", element) }.join
elsif value.respond_to?(:to_hash)
stack << [key, value.to_hash]
else
param << "#{key}=#{escape_value(value)}&"
end

stack.each do |parent, hash|
hash.each do |k, v|
if v.respond_to?(:to_hash)
stack << ["#{parent}[#{k}]", v.to_hash]
else
param << normalize_param("#{parent}[#{k}]", v)
end
end
end

param
end

# Prefer ERB::Util.url_encode, fall baack to deprecation URI.encode for
# old Ruby support
def self.escape_value(value)
if ERB::Util.respond_to? :url_encode
ERB::Util.url_encode(value.to_s)
else
URI.encode(value.to_s, Regexp.new("[^#{URI::PATTERN::UNRESERVED}]"))
end
end
end
end
34 changes: 27 additions & 7 deletions spec/client_spec.rb
Expand Up @@ -39,12 +39,6 @@ def json(data)
end
end

describe ".base_uri" do
it "should be set to customer.io's api" do
Customerio::Client.base_uri.should == "https://track.customer.io"
end
end

describe "#identify" do
it "sends a PUT request to customer.io's customer API" do
stub_request(:put, api_uri('/api/v1/customers/5')).
Expand Down Expand Up @@ -81,7 +75,7 @@ def json(data)

lambda { client.identify(:id => 5) }.should raise_error {|error|
error.should be_a Customerio::Client::InvalidResponse
error.response.code.should eq 500
error.response.code.should eq "500"
error.response.body.should eq "whatever"
}
end
Expand Down Expand Up @@ -162,6 +156,32 @@ def json(data)
client.track(5, "purchase", :type => "socks", :price => "13.99")
end

it "copes with arrays" do
stub_request(:post, api_uri('/api/v1/customers/5/events')).
with(:body => {
:name => "event",
:data => {
:things => ["a", "b", "c"]
}
}).
to_return(:status => 200, :body => "", :headers => {})

client.track(5, "event", :things => ["a", "b", "c"])
end

it "copes with hashes" do
stub_request(:post, api_uri('/api/v1/customers/5/events')).
with(:body => {
:name => "event",
:data => {
:stuff => { :a => "b" }
}
}).
to_return(:status => 200, :body => "", :headers => {})

client.track(5, "event", :stuff => { :a => "b" })
end

it "sends a POST request as json using json headers" do
client = Customerio::Client.new("SITE_ID", "API_KEY", :json => true)
data = { :type => "socks", :price => "13.99" }
Expand Down

0 comments on commit 743d545

Please sign in to comment.