hashblue-api 0.0.8

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