Skip to content

Commit

Permalink
Add Delighted::Person.list with auto pagination
Browse files Browse the repository at this point in the history
  • Loading branch information
thierryung committed Feb 25, 2020
1 parent 843fe0c commit 56267b9
Show file tree
Hide file tree
Showing 10 changed files with 216 additions and 6 deletions.
6 changes: 6 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,3 +1,9 @@
## Unreleased

Features:

- Add `Delighted::Person.list`

## 1.8.0 (2018-05-22)

Features:
Expand Down
26 changes: 24 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -49,22 +49,44 @@ updated_person1 = Delighted::Person.create(:email => "foo+test1@delighted.com",
:name => "James Scott", :send => false)
```

Listing all people:

```ruby
# List all people, auto pagination
# Note: this may use sleep to retry with back off when needed
people = Delighted::Person.list
people.auto_paging_each do |person|
# Do something with person
end

# If you do not want auto pagination to handle rate limits with sleep,
# you will need need to handle the exception manually, e.g.:
begin
people.auto_paging_each(auto_handle_rate_limits: false) do |person|
# Do something with person
end
rescue Delighted::RateLimitError => e
sleep e.response.headers["Retry-After"].to_i
retry
end
```

Unsubscribing people:

```ruby
# Unsubscribe an existing person
Delighted::Unsubscribe.create(:person_email => "foo+test1@delighted.com")
```

Listing people who have unsubscribed:
Listing people who have unsubscribed (auto pagination not supported):

```ruby
# List all people who have unsubscribed, 20 per page, first 2 pages
survey_responses_page1 = Delighted::Unsubscribe.all
survey_responses_page2 = Delighted::Unsubscribe.all(:page => 2)
```

Listing people whose emails have bounced:
Listing people whose emails have bounced (auto pagination not supported):

```ruby
# List all people whose emails have bounced, 20 per page, first 2 pages
Expand Down
2 changes: 2 additions & 0 deletions lib/delighted.rb
Original file line number Diff line number Diff line change
Expand Up @@ -9,9 +9,11 @@
require 'delighted/utils'
require 'delighted/json'

require 'delighted/list_resource'
require 'delighted/enumerable_resource_collection'
require 'delighted/resource'
require 'delighted/operations/all'
require 'delighted/operations/list'
require 'delighted/operations/create'
require 'delighted/operations/retrieve'
require 'delighted/operations/update'
Expand Down
12 changes: 9 additions & 3 deletions lib/delighted/client.rb
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ module Delighted
class Client
DEFAULT_API_BASE_URL = "https://api.delightedapp.com/v1"
DEFAULT_HTTP_ADAPTER = HTTPAdapter.new
DEFAULT_ACCEPT_HEADER = "application/json"

def initialize(opts = {})
@api_key = opts[:api_key] or raise ArgumentError, "You must provide an API key by setting Delighted.api_key = '123abc' or passing { :api_key => '123abc' } when instantiating Delighted::Client.new"
Expand All @@ -10,13 +11,18 @@ def initialize(opts = {})
end

def get_json(path, params = {})
headers = default_headers.dup.merge('Accept' => 'application/json')
request_get(path, params: params)[:json]
end

uri = URI.parse(File.join(@api_base_url, path))
def request_get(path, params: {}, accept_header: DEFAULT_ACCEPT_HEADER, full_url: false)
headers = default_headers.dup.merge('Accept' => accept_header)

path = File.join(@api_base_url, path) if !full_url
uri = URI.parse(path)
uri.query = Utils.to_query(params) unless params.empty?

response = @http_adapter.request(:get, uri, headers)
handle_json_response(response)
{json: handle_json_response(response), response: response}
end

def post_json(path, params = {})
Expand Down
17 changes: 17 additions & 0 deletions lib/delighted/http_response.rb
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,11 @@ def content_type
get_header_value("content-type")
end

def next_link
link_header = get_header_value("link")
parse_link_header(link_header)[:next]
end

def retry_after
if value = get_header_value("retry-after")
value.to_i
Expand All @@ -37,5 +42,17 @@ def get_header_value(key)
end
end
end

def parse_link_header(header_value)
links = {}
# Parse each part into a named link
header_value.split(',').each do |part, index|
section = part.split(';')
url = section[0][/<(.*)>/,1]
name = section[1][/rel="(.*)"/,1].to_sym
links[name] = url
end if !header_value.nil? && !header_value.empty?
links
end
end
end
40 changes: 40 additions & 0 deletions lib/delighted/list_resource.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
module Delighted
class ListResource
def initialize(klass, path, opts, client)
@class = klass
@path = path
@opts = opts
@client = client
@iteration_count = 0
end

def auto_paging_each(auto_handle_rate_limits: true)
loop do
begin
# Get next (or first) page
if @iteration_count == 0
data = @client.request_get(@path, params: @opts)
else
data = @client.request_get(@next_link, full_url: true)
end
rescue Delighted::RateLimitedError => e
if auto_handle_rate_limits
sleep e.response.headers['Retry-After'].to_i
retry
else
raise
end
end

@iteration_count += 1
@next_link = data[:response].next_link

data[:json].map do |attributes|
yield Object.const_get(@class).new(attributes)
end

break if @next_link.nil?
end
end
end
end
15 changes: 15 additions & 0 deletions lib/delighted/operations/list.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
module Delighted
module Operations
module List
def self.included(klass)
klass.extend(ClassMethods)
end

module ClassMethods
def list(opts = {}, client = Delighted.shared_client)
ListResource.new(self.name, path, Utils.serialize_values(opts), client)
end
end
end
end
end
1 change: 1 addition & 0 deletions lib/delighted/resources/person.rb
Original file line number Diff line number Diff line change
Expand Up @@ -4,5 +4,6 @@ class Person < Resource

include Operations::Create
include Operations::Delete
include Operations::List
end
end
2 changes: 1 addition & 1 deletion lib/delighted/version.rb
Original file line number Diff line number Diff line change
@@ -1,3 +1,3 @@
module Delighted
VERSION = "1.8.0"
VERSION = "1.9.0"
end
101 changes: 101 additions & 0 deletions test/delighted_test.rb
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,107 @@ def test_retrieving_metrics
end

class Delighted::PeopleTest < Delighted::TestCase
def test_listing_people_auto_paginate
uri = URI.parse("https://api.delightedapp.com/v1/people")
uri_next = URI.parse("http://api.delightedapp.com/v1/people.json?page_info=123456789")
headers = { "Authorization" => @auth_header, "Accept" => "application/json", "User-Agent" => "Delighted RubyGem #{Delighted::VERSION}" }

# First request mock
example_person1 = {:person_id => "4945", :email => "foo@example.com", :name => "Gold"}
example_person2 = {:person_id => "4946", :email => "foo+2@example.com", :name => "Silver"}
response = Delighted::HTTPResponse.new(200, {"Link" => "<#{uri_next}>; rel=\"next\""}, Delighted::JSON.dump([example_person1,example_person2]))
mock_http_adapter.expects(:request).with(:get, uri, headers).once.returns(response)

# Next request mock
example_person_next = {:person_id => "4947", :email => "foo+3@example.com", :name => "Bronze"}
response = Delighted::HTTPResponse.new(200, {}, Delighted::JSON.dump([example_person_next]))
mock_http_adapter.expects(:request).with(:get, uri_next, headers).once.returns(response)

persons_all = []
Delighted::Person.list.auto_paging_each do |p|
persons_all << p
end

assert_equal 3, persons_all.size

first_person = persons_all[0]
assert_kind_of Delighted::Person, first_person
assert_equal "Gold", first_person.name
assert_equal example_person1, first_person.to_hash
second_person = persons_all[1]
assert_kind_of Delighted::Person, second_person
assert_equal "Silver", second_person.name
assert_equal example_person2, second_person.to_hash
third_person = persons_all[2]
assert_kind_of Delighted::Person, third_person
assert_equal "Bronze", third_person.name
assert_equal example_person_next, third_person.to_hash
end

def test_listing_people_rate_limited
uri = URI.parse("https://api.delightedapp.com/v1/people")
uri_next = URI.parse("http://api.delightedapp.com/v1/people.json?page_info=123456789")
headers = { "Authorization" => @auth_header, "Accept" => "application/json", "User-Agent" => "Delighted RubyGem #{Delighted::VERSION}" }

# First request mock
example_person1 = {:person_id => "4945", :email => "foo@example.com", :name => "Gold"}
response = Delighted::HTTPResponse.new(200, {"Link" => "<#{uri_next}>; rel=\"next\""}, Delighted::JSON.dump([example_person1]))
mock_http_adapter.expects(:request).with(:get, uri, headers).once.returns(response)

# Next rate limited request mock
response = Delighted::HTTPResponse.new(429, { "Retry-After" => "10" }, {})
mock_http_adapter.expects(:request).with(:get, uri_next, headers).once.returns(response)

persons_all = []
exception = assert_raises Delighted::RateLimitedError do
Delighted::Person.list.auto_paging_each(auto_handle_rate_limits: false) do |p|
persons_all << p
end
end

assert_equal "10", exception.response.headers['Retry-After']

assert_equal 1, persons_all.size
first_person = persons_all[0]
assert_kind_of Delighted::Person, first_person
assert_equal "Gold", first_person.name
assert_equal example_person1, first_person.to_hash
end

def test_listing_people_auto_handle_rate_limits
uri = URI.parse("https://api.delightedapp.com/v1/people")
uri_next = URI.parse("http://api.delightedapp.com/v1/people.json?page_info=123456789")
headers = { "Authorization" => @auth_header, "Accept" => "application/json", "User-Agent" => "Delighted RubyGem #{Delighted::VERSION}" }

# First request mock
example_person1 = {:person_id => "4945", :email => "foo@example.com", :name => "Gold"}
response = Delighted::HTTPResponse.new(200, {"Link" => "<#{uri_next}>; rel=\"next\""}, Delighted::JSON.dump([example_person1]))
mock_http_adapter.expects(:request).with(:get, uri, headers).once.returns(response)

# Next rate limited request mock, then accepted request
response_rate_limited = Delighted::HTTPResponse.new(429, { "Retry-After" => "3" }, {})
example_person_next = {:person_id => "4947", :email => "foo+next@example.com", :name => "Silver"}
response_ok = Delighted::HTTPResponse.new(200, {}, Delighted::JSON.dump([example_person_next]))
mock_http_adapter.expects(:request).with(:get, uri_next, headers).twice.returns(response_rate_limited, response_ok)

persons_all = []
people = Delighted::Person.list
people.expects(:sleep).with(3)
people.auto_paging_each(auto_handle_rate_limits: true) do |p|
persons_all << p
end

assert_equal 2, persons_all.size
first_person = persons_all[0]
assert_kind_of Delighted::Person, first_person
assert_equal "Gold", first_person.name
assert_equal example_person1, first_person.to_hash
next_person = persons_all[1]
assert_kind_of Delighted::Person, next_person
assert_equal "Silver", next_person.name
assert_equal example_person_next, next_person.to_hash
end

def test_creating_or_updating_a_person
uri = URI.parse("https://api.delightedapp.com/v1/people")
headers = { 'Authorization' => @auth_header, "Accept" => "application/json", 'Content-Type' => 'application/json', 'User-Agent' => "Delighted RubyGem #{Delighted::VERSION}" }
Expand Down

0 comments on commit 56267b9

Please sign in to comment.