hashblue-api 0.0.8
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- data/README +21 -0
- data/Rakefile +98 -0
- data/hashblue-api.gemspec +58 -0
- data/lib/dependency/mime_type.rb +231 -0
- data/lib/dependency/mime_types.rb +23 -0
- data/lib/hashblue/api.rb +89 -0
- data/lib/hashblue/api/contact.rb +26 -0
- data/lib/hashblue/api/error.rb +39 -0
- data/lib/hashblue/api/message.rb +37 -0
- data/lib/hashblue/api/model.rb +55 -0
- data/lib/hashblue/api/request.rb +36 -0
- data/lib/hashblue/api/subscriber.rb +65 -0
- data/lib/hashblue/api/test_helper.rb +87 -0
- data/test/helpers/test_helper_test.rb +115 -0
- data/test/test_helper.rb +32 -0
- data/test/unit/api_test.rb +17 -0
- data/test/unit/contact_test.rb +27 -0
- data/test/unit/message_api_test.rb +135 -0
- data/test/unit/message_test.rb +42 -0
- data/test/unit/model_test.rb +27 -0
- data/test/unit/subscriber_test.rb +42 -0
- metadata +110 -0
@@ -0,0 +1,26 @@
|
|
1
|
+
# A Contact represents another number with which the Subscriber
|
2
|
+
# has sent or received Messages. It should not be instantiated
|
3
|
+
# directly; instead, use Subscriber#contacts to retrieve the
|
4
|
+
# set for a Subscriber.
|
5
|
+
class Hashblue::API::Contact < Hashblue::API::Model
|
6
|
+
#:nodoc:
|
7
|
+
def initialize(attributes)
|
8
|
+
super
|
9
|
+
@attributes["latest_message"] = Hashblue::API::Message.new(@attributes["latest_message"]) if @attributes["latest_message"]
|
10
|
+
end
|
11
|
+
|
12
|
+
# Returns an Array of Message objects corresponding to the conversation
|
13
|
+
# between the Subscriber and this Contact.
|
14
|
+
#
|
15
|
+
# The options available to this message correspond to those available for
|
16
|
+
# the collections on Subscriber.
|
17
|
+
def messages(options={})
|
18
|
+
Hashblue::API::Message.from_json(get("#{_path}/messages.json", options))
|
19
|
+
end
|
20
|
+
|
21
|
+
private
|
22
|
+
|
23
|
+
def _path
|
24
|
+
"/subscribers/#{subscriber_id}/contacts/#{id}"
|
25
|
+
end
|
26
|
+
end
|
@@ -0,0 +1,39 @@
|
|
1
|
+
# The base Hashblue Error class, which will be sent serialized
|
2
|
+
# as JSON from the API itself.
|
3
|
+
class Hashblue::API::Error < StandardError
|
4
|
+
def self.from_response(response)
|
5
|
+
if response.json? && response.keys == ["error"]
|
6
|
+
response["error"].keys.first.constantize.new(response["error"].values.first)
|
7
|
+
elsif response.code == 401
|
8
|
+
Hashblue::API::AccessDeniedError.new(response)
|
9
|
+
else
|
10
|
+
Hashblue::API::BadResponseError.new(response)
|
11
|
+
end
|
12
|
+
end
|
13
|
+
|
14
|
+
def initialize(response)
|
15
|
+
if response.respond_to?(:body)
|
16
|
+
@body = response.body
|
17
|
+
@headers = response.headers
|
18
|
+
else
|
19
|
+
@response = response
|
20
|
+
end
|
21
|
+
end
|
22
|
+
|
23
|
+
def status
|
24
|
+
:not_found
|
25
|
+
end
|
26
|
+
|
27
|
+
def to_s
|
28
|
+
str = "<##{self.class.name}:#{self.object_id}"
|
29
|
+
if @body && @headers
|
30
|
+
str + "@headers=#{@headers.inspect} @body=#{@body.inspect}>"
|
31
|
+
else
|
32
|
+
str + "@resposne=#{@response.inspect}>"
|
33
|
+
end
|
34
|
+
end
|
35
|
+
|
36
|
+
def to_json
|
37
|
+
{self.class.name => self.message}
|
38
|
+
end
|
39
|
+
end
|
@@ -0,0 +1,37 @@
|
|
1
|
+
require 'hashblue/api'
|
2
|
+
|
3
|
+
# A Message represents an SMS either sent or received by this Subscriber.
|
4
|
+
# It should not be instantiated directly; Messages can be loaded either
|
5
|
+
# using Subscriber#messages, or Contact#messages.
|
6
|
+
class Hashblue::API::Message < Hashblue::API::Model
|
7
|
+
# Deletes a specific message. Note that you must be authenticated as the
|
8
|
+
# Subscriber in order to delete a message; otherwise an AccessDeniedError
|
9
|
+
# will be raised.
|
10
|
+
def self.delete(subscriber_id, id)
|
11
|
+
Hashblue::API.delete("/subscribers/#{subscriber_id}/messages/#{id}.json")
|
12
|
+
end
|
13
|
+
|
14
|
+
#:nodoc:
|
15
|
+
def initialize(attributes = {})
|
16
|
+
super
|
17
|
+
@attributes["timestamp"] = Time.parse(@attributes["timestamp"]) if @attributes["timestamp"]
|
18
|
+
end
|
19
|
+
|
20
|
+
# Returns true if the message was sent by the Subscriber
|
21
|
+
# See also +received?+, which is the inverse of this.
|
22
|
+
def sent?
|
23
|
+
sent
|
24
|
+
end
|
25
|
+
|
26
|
+
# Returns true if the message was received by the Subscriber
|
27
|
+
# See also +sent?+, which is the inverse of this.
|
28
|
+
def received?
|
29
|
+
!sent
|
30
|
+
end
|
31
|
+
|
32
|
+
# Deletes this message. Note that deletion is not permanent; the Subscriber
|
33
|
+
# provides a collection of deleted messages.
|
34
|
+
def delete
|
35
|
+
self.class.delete(self.subscriber_id, self.id)
|
36
|
+
end
|
37
|
+
end
|
@@ -0,0 +1,55 @@
|
|
1
|
+
require 'active_support/inflector'
|
2
|
+
|
3
|
+
class Hashblue::API::Model #:nodoc:all
|
4
|
+
|
5
|
+
# Stolen from ActiveModel to get around incompatabilities between ActiveSupport 2.3.5 and ActiveModel 3.0
|
6
|
+
class Name < String
|
7
|
+
attr_reader :human, :singular, :plural, :element, :collection, :partial_path
|
8
|
+
alias_method :cache_key, :collection
|
9
|
+
|
10
|
+
def initialize(name)
|
11
|
+
super(name)
|
12
|
+
@klass = name
|
13
|
+
@singular = ActiveSupport::Inflector.underscore(self).tr('/', '_').freeze
|
14
|
+
@plural = ActiveSupport::Inflector.pluralize(@singular).freeze
|
15
|
+
@element = ActiveSupport::Inflector.underscore(ActiveSupport::Inflector.demodulize(self)).freeze
|
16
|
+
@human = ActiveSupport::Inflector.humanize(@element).freeze
|
17
|
+
@collection = ActiveSupport::Inflector.tableize(self).freeze
|
18
|
+
@partial_path = "#{@collection}/#{@element}".freeze
|
19
|
+
end
|
20
|
+
end
|
21
|
+
|
22
|
+
def self.model_name
|
23
|
+
@_model_name ||= Name.new(self.name.from(15))
|
24
|
+
end
|
25
|
+
|
26
|
+
def self.from_json(models)
|
27
|
+
models.map do |attributes|
|
28
|
+
new(attributes)
|
29
|
+
end
|
30
|
+
end
|
31
|
+
|
32
|
+
def initialize(attributes = {})
|
33
|
+
@attributes = attributes.stringify_keys
|
34
|
+
end
|
35
|
+
|
36
|
+
def method_missing(method, *args, &block)
|
37
|
+
if @attributes.keys.include?(method.to_s)
|
38
|
+
@attributes[method.to_s]
|
39
|
+
else
|
40
|
+
super
|
41
|
+
end
|
42
|
+
end
|
43
|
+
|
44
|
+
def id
|
45
|
+
@attributes["id"]
|
46
|
+
end
|
47
|
+
|
48
|
+
def to_param
|
49
|
+
id
|
50
|
+
end
|
51
|
+
|
52
|
+
def get(path, query_options={})
|
53
|
+
Hashblue::API.get(path, :query => query_options)
|
54
|
+
end
|
55
|
+
end
|
@@ -0,0 +1,36 @@
|
|
1
|
+
module Hashblue::API::Request #:nodoc:all
|
2
|
+
def get(path, options={})
|
3
|
+
_request { super }
|
4
|
+
end
|
5
|
+
|
6
|
+
def delete(path)
|
7
|
+
_request { super }
|
8
|
+
end
|
9
|
+
|
10
|
+
def _request
|
11
|
+
_with_timeout do
|
12
|
+
response = Response.new(yield)
|
13
|
+
if response.success? && response.json?
|
14
|
+
return response
|
15
|
+
else
|
16
|
+
raise Hashblue::API::Error.from_response(response)
|
17
|
+
end
|
18
|
+
end
|
19
|
+
end
|
20
|
+
|
21
|
+
def _with_timeout(seconds = default_options[:timeout], &block)
|
22
|
+
Timeout::timeout(seconds, &block)
|
23
|
+
rescue Timeout::Error
|
24
|
+
raise Hashblue::API::NotRespondingError
|
25
|
+
end
|
26
|
+
|
27
|
+
class Response < DelegateClass(HTTParty::Response)
|
28
|
+
def success?
|
29
|
+
code == 200
|
30
|
+
end
|
31
|
+
|
32
|
+
def json?
|
33
|
+
Mime::JSON === headers["content-type"].first.split(";").first.strip
|
34
|
+
end
|
35
|
+
end
|
36
|
+
end
|
@@ -0,0 +1,65 @@
|
|
1
|
+
# A Subscriber represents an individual phone number (MSISDN)
|
2
|
+
# within the Hashblue API, and is the object from which all
|
3
|
+
# other API information is made available.
|
4
|
+
#
|
5
|
+
# Create a Subscriber using the 'find' method, and supplying
|
6
|
+
# your API subscriber ID:
|
7
|
+
#
|
8
|
+
# subscriber = Hashblue::API::Subscriber.find("yourid")
|
9
|
+
# subscriber.messages
|
10
|
+
# subscriber.contacts.first.messages
|
11
|
+
# # etc..
|
12
|
+
#
|
13
|
+
# == Collection Options
|
14
|
+
#
|
15
|
+
# You can pass an options Hash to the collection methods. These can
|
16
|
+
# include:
|
17
|
+
#
|
18
|
+
# [:page] the page of messages to return
|
19
|
+
# [:per_page] the number of messages to return in a page. If the
|
20
|
+
# the page option is provided, but this is omitted, a
|
21
|
+
# default of 20 will be used by the API.
|
22
|
+
#
|
23
|
+
|
24
|
+
class Hashblue::API::Subscriber < Hashblue::API::Model
|
25
|
+
# Instantiate a Subscriber
|
26
|
+
def self.find(id)
|
27
|
+
new("id" => id)
|
28
|
+
end
|
29
|
+
|
30
|
+
# Returns an Array of Contact objects for the Subscriber.
|
31
|
+
#
|
32
|
+
# Contacts are derived from the messages sent and received by a subscriber,
|
33
|
+
# and cannot be created or manipulated directly.
|
34
|
+
#
|
35
|
+
# See above for possible options this method takes.
|
36
|
+
def contacts(options = {})
|
37
|
+
Hashblue::API::Contact.from_json(get("#{_path}/contacts.json", options))
|
38
|
+
end
|
39
|
+
|
40
|
+
# Returns an Array of Message objects for the Subscriber.
|
41
|
+
#
|
42
|
+
# All sent and received messages for all contacts are included in the
|
43
|
+
# returned Array.
|
44
|
+
#
|
45
|
+
# See above for possible options this method takes.
|
46
|
+
def messages(options={})
|
47
|
+
Hashblue::API::Message.from_json(get("#{_path}/messages.json", options))
|
48
|
+
end
|
49
|
+
|
50
|
+
# Returns an Array of Message objects for the Subscriber.
|
51
|
+
#
|
52
|
+
# All messages that have been deleted for all contacts are included in
|
53
|
+
# the returned Array.
|
54
|
+
#
|
55
|
+
# See above for possible options this method takes.
|
56
|
+
def deleted_messages(options={})
|
57
|
+
Hashblue::API::Message.from_json(get("#{_path}/messages/deleted.json", options))
|
58
|
+
end
|
59
|
+
|
60
|
+
private
|
61
|
+
|
62
|
+
def _path
|
63
|
+
"/subscribers/#{id}"
|
64
|
+
end
|
65
|
+
end
|
@@ -0,0 +1,87 @@
|
|
1
|
+
require 'hashblue/api'
|
2
|
+
require 'json'
|
3
|
+
|
4
|
+
# = Test helper for applications using the Hashblue API gem.
|
5
|
+
#
|
6
|
+
# Use this helper to stub out calls to the Hashblue API with assurance
|
7
|
+
# that stubbing will not allow your application to drift from the current
|
8
|
+
# API specification.
|
9
|
+
#
|
10
|
+
# == Usage
|
11
|
+
#
|
12
|
+
# class MyTest < Test::Unit::TestCase
|
13
|
+
# include Hashblue::API::TestHelper
|
14
|
+
#
|
15
|
+
# def test_should_work_with_the_api
|
16
|
+
# stub_hashblue_api('123', :messages => [{:content => "blah"}])
|
17
|
+
#
|
18
|
+
# subscriber = Hashblue::API::Subscriber.new('123')
|
19
|
+
#
|
20
|
+
# assert_equal 1, messages.length
|
21
|
+
# assert_equal "blah", messages[0].content
|
22
|
+
# # etc
|
23
|
+
#
|
24
|
+
# The test helper will ensure that the data you are passing to +stub_hashblue_api+
|
25
|
+
# matches the data that will be returned to your application by the API.
|
26
|
+
module Hashblue::API::TestHelper
|
27
|
+
API_KEYS = %w(contact_msisdn content timestamp id sent).map(&:to_sym)
|
28
|
+
|
29
|
+
def stub_hashblue_api(subscriber_id, options={})
|
30
|
+
options.reverse_merge!(:contacts => [], :messages => [])
|
31
|
+
|
32
|
+
messages = options[:messages].map do |attributes|
|
33
|
+
{:id => __next_firehose_id, :content => "Hello", :contact_msisdn => "123",
|
34
|
+
:subscriber_id => subscriber_id, :sent => false,
|
35
|
+
:timestamp => Time.zone.now.to_json}.merge(prepare_and_check_attributes(attributes))
|
36
|
+
end
|
37
|
+
|
38
|
+
messages.each do |message|
|
39
|
+
Hashblue::API.stubs(:delete).with("/subscribers/#{subscriber_id}/messages/#{message[:id]}.json")
|
40
|
+
end
|
41
|
+
|
42
|
+
if options[:q]
|
43
|
+
search_results = messages.select { |message| message[:content][/#{options[:q]}/] }
|
44
|
+
Hashblue::API.stubs(:get).with("/subscribers/#{subscriber_id}/messages.json",
|
45
|
+
{:query => {:q => options[:q]}}).returns(search_results)
|
46
|
+
Hashblue::API.stubs(:get).with("/subscribers/#{subscriber_id}/messages.json", {:query => {}}).returns(messages)
|
47
|
+
else
|
48
|
+
Hashblue::API.stubs(:get).with("/subscribers/#{subscriber_id}/messages.json", anything).returns(messages)
|
49
|
+
end
|
50
|
+
|
51
|
+
contacts = messages.group_by { |m| m[:contact_msisdn] }.map do |msisdn, contact_messages|
|
52
|
+
explicit_contact = options[:contacts].find { |c| c[:msisdn] == msisdn }
|
53
|
+
id = explicit_contact ? explicit_contact[:id] : __next_firehose_id
|
54
|
+
|
55
|
+
url = "/subscribers/#{subscriber_id}/contacts/#{id}/messages.json"
|
56
|
+
Hashblue::API.stubs(:get).with(url, anything).returns(contact_messages)
|
57
|
+
|
58
|
+
{:id => id, :msisdn => msisdn, :latest_message => contact_messages.first, :subscriber_id => subscriber_id}
|
59
|
+
end
|
60
|
+
|
61
|
+
contacts_with_no_messages = options[:contacts].reject do |contact|
|
62
|
+
messages.find { |m| m[:contact_msisdn] == contact[:msisdn] }
|
63
|
+
end.map do |contact|
|
64
|
+
{:latest_message => nil, :id => __next_firehose_id}.merge(contact)
|
65
|
+
end
|
66
|
+
|
67
|
+
contacts += contacts_with_no_messages
|
68
|
+
|
69
|
+
Hashblue::API.stubs(:get).with("/subscribers/#{subscriber_id}/contacts.json", anything).returns(contacts)
|
70
|
+
end
|
71
|
+
|
72
|
+
private
|
73
|
+
|
74
|
+
def prepare_and_check_attributes(message)
|
75
|
+
unless (message.keys - API_KEYS).empty?
|
76
|
+
raise "#{message.keys.inspect} doesn't match the current API (#{API_KEYS.inspect})"
|
77
|
+
end
|
78
|
+
message[:timestamp] = message[:timestamp].to_s if message[:timestamp] # in case it was a DateTime
|
79
|
+
message
|
80
|
+
end
|
81
|
+
|
82
|
+
def __next_firehose_id
|
83
|
+
@@__firehose_id ||= 1
|
84
|
+
"firehose0helper0id#{@@__firehose_id += 1}"
|
85
|
+
end
|
86
|
+
|
87
|
+
end
|
@@ -0,0 +1,115 @@
|
|
1
|
+
require 'test_helper'
|
2
|
+
require 'hashblue/api/test_helper'
|
3
|
+
|
4
|
+
# Time.zone only appears when loaded within Rails
|
5
|
+
unless Time.respond_to?(:zone) && Time.zone
|
6
|
+
def Time.zone
|
7
|
+
Time
|
8
|
+
end
|
9
|
+
end
|
10
|
+
|
11
|
+
class TestHelperTest < Test::Unit::TestCase
|
12
|
+
include Hashblue::API::TestHelper
|
13
|
+
|
14
|
+
context "When stubbing a message with correct attributes for a subscriber" do
|
15
|
+
setup do
|
16
|
+
@subscriber = Hashblue::API::Subscriber.new(:id => "123")
|
17
|
+
end
|
18
|
+
|
19
|
+
should "no errors raised" do
|
20
|
+
assert_nothing_raised do
|
21
|
+
stub_hashblue_api(@subscriber_id)
|
22
|
+
end
|
23
|
+
end
|
24
|
+
|
25
|
+
should "stub messages for subscriber" do
|
26
|
+
messages = [{:content => "Blah", :contact_msisdn => "123", :timestamp => 1.day.ago}]
|
27
|
+
stub_hashblue_api(@subscriber.id, :messages => messages)
|
28
|
+
|
29
|
+
messages = @subscriber.messages
|
30
|
+
assert_equal 1, messages.length
|
31
|
+
assert_equal "Blah", messages.first.content
|
32
|
+
end
|
33
|
+
|
34
|
+
should "stub deleting a message" do
|
35
|
+
messages = [{:id => "123"}]
|
36
|
+
stub_hashblue_api(@subscriber.id, :messages => messages)
|
37
|
+
|
38
|
+
assert_nothing_raised do
|
39
|
+
Hashblue::API::Message.delete(@subscriber.id, "123")
|
40
|
+
end
|
41
|
+
end
|
42
|
+
|
43
|
+
should "stub querying messages by keyword" do
|
44
|
+
stub_hashblue_api(@subscriber.id, :messages => [{:id => "123", :content => "I'll meet you at the office"},
|
45
|
+
{:id => "456", :content => "sure thing we'll do that"}], :q => "office")
|
46
|
+
messages = @subscriber.messages(:q => "office")
|
47
|
+
assert_equal 1, messages.length
|
48
|
+
assert_equal "I'll meet you at the office", messages.first.content
|
49
|
+
end
|
50
|
+
|
51
|
+
should "return no messages or contacts if none are provided" do
|
52
|
+
stub_hashblue_api(@subscriber.id)
|
53
|
+
assert_equal [], @subscriber.messages
|
54
|
+
assert_equal [], @subscriber.contacts
|
55
|
+
end
|
56
|
+
|
57
|
+
should "provide default content for stubbed messages if it is not provided" do
|
58
|
+
stub_hashblue_api(@subscriber.id, :messages => [{:contact_msisdn => "123"}])
|
59
|
+
|
60
|
+
assert_kind_of String, @subscriber.messages.first.content
|
61
|
+
assert @subscriber.messages.first.content.length > 0
|
62
|
+
end
|
63
|
+
|
64
|
+
should "stub contacts for the subscriber based on the messages" do
|
65
|
+
stub_hashblue_api(@subscriber.id, :messages => [{:contact_msisdn => "123"}])
|
66
|
+
|
67
|
+
contacts = @subscriber.contacts
|
68
|
+
assert_equal 1, contacts.length
|
69
|
+
assert_equal "123", contacts.first.msisdn
|
70
|
+
end
|
71
|
+
|
72
|
+
should "stub messages per contact" do
|
73
|
+
stub_hashblue_api(@subscriber.id, :messages => [{:content => "Hello", :contact_msisdn => "123"},
|
74
|
+
{:content => "Wotcha", :contact_msisdn => "456"}])
|
75
|
+
|
76
|
+
contacts = @subscriber.contacts
|
77
|
+
assert_equal "Hello", contacts.find { |c| c.msisdn == "123" }.messages.first.content
|
78
|
+
assert_equal "Wotcha", contacts.find { |c| c.msisdn == "456" }.messages.first.content
|
79
|
+
end
|
80
|
+
|
81
|
+
should "stub contacts even when they have no messages" do
|
82
|
+
stub_hashblue_api(@subscriber.id, :messages => [{:content => "Hello", :contact_msisdn => "123"},
|
83
|
+
{:content => "Goodbye", :contact_msisdn => "456"}],
|
84
|
+
:contacts => [{:id => "abc", :msisdn => "123"}])
|
85
|
+
|
86
|
+
contacts = @subscriber.contacts
|
87
|
+
assert_equal 2, contacts.length
|
88
|
+
assert_equal ["123", "456"], contacts.map(&:msisdn)
|
89
|
+
end
|
90
|
+
|
91
|
+
should "stub contacts even when there are no messages" do
|
92
|
+
stub_hashblue_api(@subscriber.id, :contacts => [{:id => "abc", :msisdn => "123"}])
|
93
|
+
|
94
|
+
contacts = @subscriber.contacts
|
95
|
+
assert_equal 1, contacts.length
|
96
|
+
assert_nil contacts.first.latest_message
|
97
|
+
end
|
98
|
+
|
99
|
+
should "allow control over contact ids" do
|
100
|
+
stub_hashblue_api(@subscriber.id, :messages => [{:contact_msisdn => "123"}],
|
101
|
+
:contacts => [{:id => "abc", :msisdn => "123"}])
|
102
|
+
|
103
|
+
contact = @subscriber.contacts.first
|
104
|
+
assert_equal "123", contact.msisdn
|
105
|
+
assert_equal "abc", contact.id
|
106
|
+
end
|
107
|
+
|
108
|
+
should "allow control over message ids" do
|
109
|
+
stub_hashblue_api(@subscriber.id, :messages => [{:id => "abc"}])
|
110
|
+
|
111
|
+
message = @subscriber.messages.first
|
112
|
+
assert_equal "abc", message.id
|
113
|
+
end
|
114
|
+
end
|
115
|
+
end
|