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 ADDED
@@ -0,0 +1,21 @@
1
+ = It's the Hashblue API gem
2
+
3
+ For usage, look at Hashblue::API (in lib/firehose/api.rb)
4
+
5
+
6
+ = Short usage example
7
+
8
+ require 'active_support/all' # unless you have already loaded it
9
+ require 'hashblue/api'
10
+
11
+ Hashblue::API.base_uri "https://api.hashblue.apigee.com"
12
+ Hashblue::API.basic_auth "dave@example.com", "yourapikey"
13
+
14
+ subscriber = Hashblue::API::Subscriber.find("yourapikey")
15
+
16
+ subscriber.messages
17
+
18
+
19
+ = Testing against the Hashblue API
20
+
21
+ To learn about stubbing out the Hashblue API, see Hashblue::API::TestHelper
data/Rakefile ADDED
@@ -0,0 +1,98 @@
1
+ require "rubygems"
2
+ require "rake/gempackagetask"
3
+ require "rake/rdoctask"
4
+ require "rake/testtask"
5
+
6
+ task :default => :test
7
+
8
+ # This builds the actual gem. For details of what all these options
9
+ # mean, and other ones you can add, check the documentation here:
10
+ #
11
+ # http://rubygems.org/read/chapter/20
12
+ #
13
+ path = File.expand_path("..", __FILE__)
14
+
15
+ spec = Dir.chdir(path) do
16
+ Gem::Specification.new do |s|
17
+
18
+ # Change these as appropriate
19
+ s.name = "hashblue-api"
20
+ s.version = "0.0.8"
21
+ s.summary = "It's the Hashblue API, stupid!"
22
+ s.author = "FreeRange"
23
+ s.email = "lets@gofreerange.com"
24
+ s.homepage = "http://gofreerange.com"
25
+
26
+ s.has_rdoc = true
27
+ s.extra_rdoc_files = %w(README)
28
+ s.rdoc_options = %w(--main README)
29
+
30
+ # Add any extra files to include in the gem
31
+ s.files = `cd #{path} && git ls-files`.split("\n").sort
32
+
33
+ # You need to put your code in a directory which can then be added to
34
+ # the $LOAD_PATH by rubygems. Typically this is lib, but you don't seem
35
+ # to have that directory. You'll need to set the line below to whatever
36
+ # directory your code is in. Rubygems is going to assume lib if you leave
37
+ # this blank.
38
+ #
39
+ s.require_paths = ["lib"]
40
+
41
+ # If you want to depend on other gems, add them here, along with any
42
+ # relevant versions
43
+ s.add_dependency 'httparty', '0.5.2'
44
+ s.add_dependency 'activesupport', '>= 2.3.5'
45
+
46
+ # If your tests use any gems, include them here
47
+ # s.add_development_dependency("mocha") # for example
48
+ end
49
+ end
50
+
51
+ # Stolen from jeweler
52
+ def prettyify_array(gemspec_ruby, array_name)
53
+ gemspec_ruby.gsub(/s\.#{array_name.to_s} = \[.+?\]/) do |match|
54
+ leadin, files = match[0..-2].split("[")
55
+ leadin + "[\n #{files.split(",").join(",\n ")}\n ]"
56
+ end
57
+ end
58
+
59
+ Rake::TestTask.new do |t|
60
+ t.libs << "test"
61
+ t.test_files = FileList["#{path}/test/**/*test.rb"]
62
+ t.verbose = true
63
+ end
64
+
65
+ # This task actually builds the gem. We also regenerate a static
66
+ # .gemspec file, which is useful if something (i.e. GitHub) will
67
+ # be automatically building a gem for this project. If you're not
68
+ # using GitHub, edit as appropriate.
69
+ #
70
+ # To publish your gem online, install the 'gemcutter' gem; Read more
71
+ # about that here: http://gemcutter.org/pages/gem_docs
72
+ Rake::GemPackageTask.new(spec) do |pkg|
73
+ pkg.gem_spec = spec
74
+ end
75
+
76
+ task :gemspec do
77
+ output = spec.to_ruby
78
+ output = prettyify_array(output, :files)
79
+ output = prettyify_array(output, :test_files)
80
+ output = prettyify_array(output, :extra_rdoc_files)
81
+
82
+ file = File.expand_path("../#{spec.name}.gemspec", __FILE__)
83
+ File.open(file, "w") {|f| f << output }
84
+ end
85
+
86
+ task :package => :gemspec
87
+
88
+ # Generate documentation
89
+ Rake::RDocTask.new do |rd|
90
+ rd.main = "README"
91
+ rd.rdoc_files.include("README", "lib/**/*.rb")
92
+ rd.rdoc_dir = "rdoc"
93
+ end
94
+
95
+ desc 'Clear out RDoc and generated packages'
96
+ task :clean => [:clobber_rdoc, :clobber_package] do
97
+ rm "#{spec.name}.gemspec"
98
+ end
@@ -0,0 +1,58 @@
1
+ # -*- encoding: utf-8 -*-
2
+
3
+ Gem::Specification.new do |s|
4
+ s.name = %q{hashblue-api}
5
+ s.version = "0.0.8"
6
+
7
+ s.required_rubygems_version = Gem::Requirement.new(">= 0") if s.respond_to? :required_rubygems_version=
8
+ s.authors = ["FreeRange"]
9
+ s.date = %q{2010-05-06}
10
+ s.email = %q{lets@gofreerange.com}
11
+ s.extra_rdoc_files = [
12
+ "README"
13
+ ]
14
+ s.files = [
15
+ "README",
16
+ "Rakefile",
17
+ "hashblue-api.gemspec",
18
+ "lib/dependency/mime_type.rb",
19
+ "lib/dependency/mime_types.rb",
20
+ "lib/hashblue/api.rb",
21
+ "lib/hashblue/api/contact.rb",
22
+ "lib/hashblue/api/error.rb",
23
+ "lib/hashblue/api/message.rb",
24
+ "lib/hashblue/api/model.rb",
25
+ "lib/hashblue/api/request.rb",
26
+ "lib/hashblue/api/subscriber.rb",
27
+ "lib/hashblue/api/test_helper.rb",
28
+ "test/helpers/test_helper_test.rb",
29
+ "test/test_helper.rb",
30
+ "test/unit/api_test.rb",
31
+ "test/unit/contact_test.rb",
32
+ "test/unit/message_api_test.rb",
33
+ "test/unit/message_test.rb",
34
+ "test/unit/model_test.rb",
35
+ "test/unit/subscriber_test.rb"
36
+ ]
37
+ s.homepage = %q{http://gofreerange.com}
38
+ s.rdoc_options = ["--main", "README"]
39
+ s.require_paths = ["lib"]
40
+ s.rubygems_version = %q{1.3.6}
41
+ s.summary = %q{It's the Hashblue API, stupid!}
42
+
43
+ if s.respond_to? :specification_version then
44
+ current_version = Gem::Specification::CURRENT_SPECIFICATION_VERSION
45
+ s.specification_version = 3
46
+
47
+ if Gem::Version.new(Gem::RubyGemsVersion) >= Gem::Version.new('1.2.0') then
48
+ s.add_runtime_dependency(%q<httparty>, ["= 0.5.2"])
49
+ s.add_runtime_dependency(%q<activesupport>, [">= 2.3.5"])
50
+ else
51
+ s.add_dependency(%q<httparty>, ["= 0.5.2"])
52
+ s.add_dependency(%q<activesupport>, [">= 2.3.5"])
53
+ end
54
+ else
55
+ s.add_dependency(%q<httparty>, ["= 0.5.2"])
56
+ s.add_dependency(%q<activesupport>, [">= 2.3.5"])
57
+ end
58
+ end
@@ -0,0 +1,231 @@
1
+ require 'set'
2
+ require 'active_support/core_ext/class/attribute_accessors'
3
+
4
+ module Mime
5
+ class Mimes < Array
6
+ def symbols
7
+ @symbols ||= map {|m| m.to_sym }
8
+ end
9
+
10
+ %w(<< concat shift unshift push pop []= clear compact! collect!
11
+ delete delete_at delete_if flatten! map! insert reject! reverse!
12
+ replace slice! sort! uniq!).each do |method|
13
+ module_eval <<-CODE, __FILE__, __LINE__ + 1
14
+ def #{method}(*)
15
+ @symbols = nil
16
+ super
17
+ end
18
+ CODE
19
+ end
20
+ end
21
+
22
+ SET = Mimes.new
23
+ EXTENSION_LOOKUP = {}
24
+ LOOKUP = Hash.new { |h, k| h[k] = Type.new(k) unless k.blank? }
25
+
26
+ def self.[](type)
27
+ return type if type.is_a?(Type)
28
+ Type.lookup_by_extension(type.to_s)
29
+ end
30
+
31
+ # Encapsulates the notion of a mime type. Can be used at render time, for example, with:
32
+ #
33
+ # class PostsController < ActionController::Base
34
+ # def show
35
+ # @post = Post.find(params[:id])
36
+ #
37
+ # respond_to do |format|
38
+ # format.html
39
+ # format.ics { render :text => post.to_ics, :mime_type => Mime::Type["text/calendar"] }
40
+ # format.xml { render :xml => @people.to_xml }
41
+ # end
42
+ # end
43
+ # end
44
+ class Type
45
+ @@html_types = Set.new [:html, :all]
46
+ cattr_reader :html_types
47
+
48
+ # These are the content types which browsers can generate without using ajax, flash, etc
49
+ # i.e. following a link, getting an image or posting a form. CSRF protection
50
+ # only needs to protect against these types.
51
+ @@browser_generated_types = Set.new [:html, :url_encoded_form, :multipart_form, :text]
52
+ cattr_reader :browser_generated_types
53
+ attr_reader :symbol
54
+
55
+ @@unverifiable_types = Set.new [:text, :json, :csv, :xml, :rss, :atom, :yaml]
56
+ def self.unverifiable_types
57
+ ActiveSupport::Deprecation.warn("unverifiable_types is deprecated and has no effect", caller)
58
+ @@unverifiable_types
59
+ end
60
+
61
+ # A simple helper class used in parsing the accept header
62
+ class AcceptItem #:nodoc:
63
+ attr_accessor :order, :name, :q
64
+
65
+ def initialize(order, name, q=nil)
66
+ @order = order
67
+ @name = name.strip
68
+ q ||= 0.0 if @name == Mime::ALL # default wilcard match to end of list
69
+ @q = ((q || 1.0).to_f * 100).to_i
70
+ end
71
+
72
+ def to_s
73
+ @name
74
+ end
75
+
76
+ def <=>(item)
77
+ result = item.q <=> q
78
+ result = order <=> item.order if result == 0
79
+ result
80
+ end
81
+
82
+ def ==(item)
83
+ name == (item.respond_to?(:name) ? item.name : item)
84
+ end
85
+ end
86
+
87
+ class << self
88
+ def lookup(string)
89
+ LOOKUP[string]
90
+ end
91
+
92
+ def lookup_by_extension(extension)
93
+ EXTENSION_LOOKUP[extension.to_s]
94
+ end
95
+
96
+ # Registers an alias that's not used on mime type lookup, but can be referenced directly. Especially useful for
97
+ # rendering different HTML versions depending on the user agent, like an iPhone.
98
+ def register_alias(string, symbol, extension_synonyms = [])
99
+ register(string, symbol, [], extension_synonyms, true)
100
+ end
101
+
102
+ def register(string, symbol, mime_type_synonyms = [], extension_synonyms = [], skip_lookup = false)
103
+ Mime.instance_eval { const_set symbol.to_s.upcase, Type.new(string, symbol, mime_type_synonyms) }
104
+
105
+ SET << Mime.const_get(symbol.to_s.upcase)
106
+
107
+ ([string] + mime_type_synonyms).each { |str| LOOKUP[str] = SET.last } unless skip_lookup
108
+ ([symbol.to_s] + extension_synonyms).each { |ext| EXTENSION_LOOKUP[ext] = SET.last }
109
+ end
110
+
111
+ def parse(accept_header)
112
+ if accept_header !~ /,/
113
+ [Mime::Type.lookup(accept_header)]
114
+ else
115
+ # keep track of creation order to keep the subsequent sort stable
116
+ list = []
117
+ accept_header.split(/,/).each_with_index do |header, index|
118
+ params, q = header.split(/;\s*q=/)
119
+ if params
120
+ params.strip!
121
+ list << AcceptItem.new(index, params, q) unless params.empty?
122
+ end
123
+ end
124
+ list.sort!
125
+
126
+ # Take care of the broken text/xml entry by renaming or deleting it
127
+ text_xml = list.index("text/xml")
128
+ app_xml = list.index(Mime::XML.to_s)
129
+
130
+ if text_xml && app_xml
131
+ # set the q value to the max of the two
132
+ list[app_xml].q = [list[text_xml].q, list[app_xml].q].max
133
+
134
+ # make sure app_xml is ahead of text_xml in the list
135
+ if app_xml > text_xml
136
+ list[app_xml], list[text_xml] = list[text_xml], list[app_xml]
137
+ app_xml, text_xml = text_xml, app_xml
138
+ end
139
+
140
+ # delete text_xml from the list
141
+ list.delete_at(text_xml)
142
+
143
+ elsif text_xml
144
+ list[text_xml].name = Mime::XML.to_s
145
+ end
146
+
147
+ # Look for more specific XML-based types and sort them ahead of app/xml
148
+
149
+ if app_xml
150
+ idx = app_xml
151
+ app_xml_type = list[app_xml]
152
+
153
+ while(idx < list.length)
154
+ type = list[idx]
155
+ break if type.q < app_xml_type.q
156
+ if type.name =~ /\+xml$/
157
+ list[app_xml], list[idx] = list[idx], list[app_xml]
158
+ app_xml = idx
159
+ end
160
+ idx += 1
161
+ end
162
+ end
163
+
164
+ list.map! { |i| Mime::Type.lookup(i.name) }.uniq!
165
+ list
166
+ end
167
+ end
168
+ end
169
+
170
+ def initialize(string, symbol = nil, synonyms = [])
171
+ @symbol, @synonyms = symbol, synonyms
172
+ @string = string
173
+ end
174
+
175
+ def to_s
176
+ @string
177
+ end
178
+
179
+ def to_str
180
+ to_s
181
+ end
182
+
183
+ def to_sym
184
+ @symbol || @string.to_sym
185
+ end
186
+
187
+ def ===(list)
188
+ if list.is_a?(Array)
189
+ (@synonyms + [ self ]).any? { |synonym| list.include?(synonym) }
190
+ else
191
+ super
192
+ end
193
+ end
194
+
195
+ def ==(mime_type)
196
+ return false if mime_type.blank?
197
+ (@synonyms + [ self ]).any? do |synonym|
198
+ synonym.to_s == mime_type.to_s || synonym.to_sym == mime_type.to_sym
199
+ end
200
+ end
201
+
202
+ def =~(mime_type)
203
+ return false if mime_type.blank?
204
+ regexp = Regexp.new(Regexp.quote(mime_type.to_s))
205
+ (@synonyms + [ self ]).any? do |synonym|
206
+ synonym.to_s =~ regexp
207
+ end
208
+ end
209
+
210
+ # Returns true if Action Pack should check requests using this Mime Type for possible request forgery. See
211
+ # ActionController::RequestForgeryProtection.
212
+ def verify_request?
213
+ @@browser_generated_types.include?(to_sym)
214
+ end
215
+
216
+ def html?
217
+ @@html_types.include?(to_sym) || @string =~ /html/
218
+ end
219
+
220
+ private
221
+ def method_missing(method, *args)
222
+ if method.to_s =~ /(\w+)\?$/
223
+ $1.downcase.to_sym == to_sym
224
+ else
225
+ super
226
+ end
227
+ end
228
+ end
229
+ end
230
+
231
+ require 'dependency/mime_types'
@@ -0,0 +1,23 @@
1
+ # Build list of Mime types for HTTP responses
2
+ # http://www.iana.org/assignments/media-types/
3
+
4
+ Mime::Type.register "text/html", :html, %w( application/xhtml+xml ), %w( xhtml )
5
+ Mime::Type.register "text/plain", :text, [], %w(txt)
6
+ Mime::Type.register "text/javascript", :js, %w( application/javascript application/x-javascript )
7
+ Mime::Type.register "text/css", :css
8
+ Mime::Type.register "text/calendar", :ics
9
+ Mime::Type.register "text/csv", :csv
10
+ Mime::Type.register "application/xml", :xml, %w( text/xml application/x-xml )
11
+ Mime::Type.register "application/rss+xml", :rss
12
+ Mime::Type.register "application/atom+xml", :atom
13
+ Mime::Type.register "application/x-yaml", :yaml, %w( text/yaml )
14
+
15
+ Mime::Type.register "multipart/form-data", :multipart_form
16
+ Mime::Type.register "application/x-www-form-urlencoded", :url_encoded_form
17
+
18
+ # http://www.ietf.org/rfc/rfc4627.txt
19
+ # http://www.json.org/JSONRequest.html
20
+ Mime::Type.register "application/json", :json, %w( text/x-json application/jsonrequest )
21
+
22
+ # Create Mime::ALL but do not add it to the SET.
23
+ Mime::ALL = Mime::Type.new("*/*", :all, [])
@@ -0,0 +1,89 @@
1
+ require 'active_support/all'
2
+ require 'httparty'
3
+ require 'timeout'
4
+
5
+ # By using autoload rather than require, we only use our fallback
6
+ # dependency if Mime hasn't been loaded elsewhere
7
+ autoload :Mime, 'dependency/mime_type'
8
+
9
+ module Hashblue
10
+
11
+ # = It's the Hashblue API gem
12
+ #
13
+ # == Connecting
14
+ #
15
+ # Hashblue API requires ActiveSupport; if this isn't already loaded, you'll need
16
+ # to ensure that it is available - either by requiring rubygems, or ensuring
17
+ # it is in the +LOAD_PATH+ already.
18
+ #
19
+ # To connect to the Hashblue API, you need to set the base URL, and provide
20
+ # your credentials (you can find these in your hashblue.com account). For
21
+ # example:
22
+ #
23
+ # require 'active_support/all' # unless you have already loaded it
24
+ # require 'hashblue/api'
25
+ #
26
+ # Hashblue::API.base_uri "https://api.hashblue.apigee.com"
27
+ # Hashblue::API.basic_auth "dave@example.com", "yourapikey"
28
+ #
29
+ #
30
+ # == Using the API
31
+ #
32
+ # Everything is loaded via your subscriber:
33
+ #
34
+ # subscriber = Hashblue::API::Subscriber.find("yourapikey")
35
+ #
36
+ # Once you have this, you can load all messages, all contacts, or all
37
+ # messages for a specific contact:
38
+ #
39
+ # all_messages = subscriber.messages
40
+ # all_contacts = subscriber.contacts
41
+ # messages_between_me_and_a_contact = all_contacts.first.messages
42
+ #
43
+ # == Interacting with messages
44
+ #
45
+ # It's simple to delete a message:
46
+ #
47
+ # all_messages.first.delete
48
+ #
49
+ # This message is now deleted. You can see a list of all deleted messages
50
+ # via the subscriber:
51
+ #
52
+ # subscriber.deleted_messages
53
+ module API
54
+ autoload :Contact, 'hashblue/api/contact'
55
+ autoload :Message, 'hashblue/api/message'
56
+ autoload :Model, 'hashblue/api/model'
57
+ autoload :Request, 'hashblue/api/request'
58
+ autoload :Subscriber, 'hashblue/api/subscriber'
59
+ autoload :Error, 'hashblue/api/error'
60
+ autoload :TestHelper, 'hashblue/api/test_helper'
61
+
62
+ # Raised when the API fails to respond with a timeout. The timeout
63
+ # can be set using Hashblue::API.timeout
64
+ class NotRespondingError < StandardError; end
65
+ # Raised when the API responded in a way that we couldn't handle.
66
+ class BadResponseError < Hashblue::API::Error; end
67
+ # Raised when the object requested cannot be found.
68
+ class NotFoundError < Hashblue::API::Error; end
69
+ # Raised when the object requested is not available given the
70
+ # credentials used; most often is is because a Contact or Message
71
+ # belongs to a different Subscriber
72
+ class AccessDeniedError < Hashblue::API::Error
73
+ def status
74
+ :unauthorized
75
+ end
76
+ end
77
+
78
+ # Sets the timeout used before raising an error.
79
+ def self.timeout(value)
80
+ default_options[:timeout] = value
81
+ end
82
+
83
+ include HTTParty
84
+ extend Request
85
+
86
+ base_uri "https://api.hashblue.apigee.com"
87
+ timeout 5
88
+ end
89
+ end