hashblue-api 0.0.8

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