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.
@@ -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