hashblue-api 0.0.8
Sign up to get free protection for your applications and to get access to all the features.
- 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
|