contactology 0.0.1

Sign up to get free protection for your applications and to get access to all the features.
Files changed (86) hide show
  1. data/.gitignore +5 -0
  2. data/.infinity_test +16 -0
  3. data/.rspec +3 -0
  4. data/.rvmrc +41 -0
  5. data/.watchr +36 -0
  6. data/Gemfile +6 -0
  7. data/Rakefile +1 -0
  8. data/contactology.gemspec +28 -0
  9. data/lib/contactology.rb +125 -0
  10. data/lib/contactology/api.rb +80 -0
  11. data/lib/contactology/basic_object.rb +21 -0
  12. data/lib/contactology/campaign.rb +127 -0
  13. data/lib/contactology/campaign/preview.rb +11 -0
  14. data/lib/contactology/campaigns.rb +2 -0
  15. data/lib/contactology/campaigns/standard.rb +80 -0
  16. data/lib/contactology/campaigns/transactional.rb +58 -0
  17. data/lib/contactology/configuration.rb +42 -0
  18. data/lib/contactology/contact.rb +193 -0
  19. data/lib/contactology/errors.rb +4 -0
  20. data/lib/contactology/issue.rb +24 -0
  21. data/lib/contactology/issues.rb +18 -0
  22. data/lib/contactology/list.rb +192 -0
  23. data/lib/contactology/list_proxy.rb +25 -0
  24. data/lib/contactology/parser.rb +5 -0
  25. data/lib/contactology/send_result.rb +35 -0
  26. data/lib/contactology/stash.rb +29 -0
  27. data/lib/contactology/transactional_message.rb +38 -0
  28. data/lib/contactology/version.rb +3 -0
  29. data/spec/factories/campaigns.rb +18 -0
  30. data/spec/factories/contacts.rb +3 -0
  31. data/spec/factories/issues.rb +9 -0
  32. data/spec/factories/lists.rb +3 -0
  33. data/spec/factories/transactional_messages.rb +5 -0
  34. data/spec/fixtures/net/campaign/destroy.yml +246 -0
  35. data/spec/fixtures/net/campaign/find/failure.yml +36 -0
  36. data/spec/fixtures/net/campaign/find/success.yml +176 -0
  37. data/spec/fixtures/net/campaign/find_by_name/failure.yml +36 -0
  38. data/spec/fixtures/net/campaign/find_by_name/success.yml +211 -0
  39. data/spec/fixtures/net/campaign/preview.yml +106 -0
  40. data/spec/fixtures/net/campaigns/standard/create/failure.yml +106 -0
  41. data/spec/fixtures/net/campaigns/standard/create/invalid.yml +141 -0
  42. data/spec/fixtures/net/campaigns/standard/create/success.yml +176 -0
  43. data/spec/fixtures/net/campaigns/standard/send_campaign/failure.yml +316 -0
  44. data/spec/fixtures/net/campaigns/standard/send_campaign/success.yml +316 -0
  45. data/spec/fixtures/net/campaigns/transactional/create/failure.yml +36 -0
  46. data/spec/fixtures/net/campaigns/transactional/create/success.yml +71 -0
  47. data/spec/fixtures/net/contact/active.yml +106 -0
  48. data/spec/fixtures/net/contact/change_email/success.yml +176 -0
  49. data/spec/fixtures/net/contact/change_email/unknown.yml +36 -0
  50. data/spec/fixtures/net/contact/create.yml +106 -0
  51. data/spec/fixtures/net/contact/destroy.yml +141 -0
  52. data/spec/fixtures/net/contact/find/active.yml +106 -0
  53. data/spec/fixtures/net/contact/find/suppressed.yml +141 -0
  54. data/spec/fixtures/net/contact/find/unknown.yml +36 -0
  55. data/spec/fixtures/net/contact/lists/empty.yml +106 -0
  56. data/spec/fixtures/net/contact/lists/full.yml +246 -0
  57. data/spec/fixtures/net/contact/lists/unknown.yml +71 -0
  58. data/spec/fixtures/net/contact/suppress.yml +141 -0
  59. data/spec/fixtures/net/list/all.yml +141 -0
  60. data/spec/fixtures/net/list/create.yml +106 -0
  61. data/spec/fixtures/net/list/destroy.yml +176 -0
  62. data/spec/fixtures/net/list/find/success.yml +141 -0
  63. data/spec/fixtures/net/list/find/unknown.yml +36 -0
  64. data/spec/fixtures/net/list/import/success.yml +351 -0
  65. data/spec/fixtures/net/list/subscribe/success.yml +211 -0
  66. data/spec/fixtures/net/list/unsubscribe/success.yml +246 -0
  67. data/spec/fixtures/net/transactional_message/send_message/failure.yml +176 -0
  68. data/spec/fixtures/net/transactional_message/send_message/success.yml +176 -0
  69. data/spec/models/contactology/api_spec.rb +51 -0
  70. data/spec/models/contactology/campaign_spec.rb +75 -0
  71. data/spec/models/contactology/campaigns/standard_spec.rb +84 -0
  72. data/spec/models/contactology/campaigns/transactional_spec.rb +28 -0
  73. data/spec/models/contactology/configuration_spec.rb +29 -0
  74. data/spec/models/contactology/contact_spec.rb +175 -0
  75. data/spec/models/contactology/issues_spec.rb +34 -0
  76. data/spec/models/contactology/list_spec.rb +125 -0
  77. data/spec/models/contactology/send_result_spec.rb +65 -0
  78. data/spec/models/contactology/stash_spec.rb +65 -0
  79. data/spec/models/contactology/transactional_message_spec.rb +65 -0
  80. data/spec/models/contactology_spec.rb +67 -0
  81. data/spec/requests/contacts_spec.rb +4 -0
  82. data/spec/spec_helper.rb +15 -0
  83. data/spec/support/contactology.rb +34 -0
  84. data/spec/support/factory_girl.rb +19 -0
  85. data/spec/support/vcr.rb +11 -0
  86. metadata +282 -0
data/.gitignore ADDED
@@ -0,0 +1,5 @@
1
+ *.gem
2
+ .bundle
3
+ Gemfile.lock
4
+ pkg/*
5
+ spec/contactology.yml
data/.infinity_test ADDED
@@ -0,0 +1,16 @@
1
+ # encoding: utf-8
2
+
3
+ infinity_test do
4
+ notifications :growl
5
+ use :rubies => %w(1.9.2-p180 1.8.7-p334), :gemset => 'contactology', :test_framework => :rspec
6
+
7
+ before_run do
8
+ clear :terminal
9
+ end
10
+
11
+ heuristics do
12
+ add('lib/') do |file|
13
+ run :all => :tests
14
+ end
15
+ end
16
+ end
data/.rspec ADDED
@@ -0,0 +1,3 @@
1
+ --color
2
+ --format documentation
3
+ --tty
data/.rvmrc ADDED
@@ -0,0 +1,41 @@
1
+ #!/usr/bin/env bash
2
+
3
+ # This is an RVM Project .rvmrc file, used to automatically load the ruby
4
+ # development environment upon cd'ing into the directory
5
+
6
+ # First we specify our desired <ruby>[@<gemset>], the @gemset name is optional.
7
+ environment_id="ruby-1.9.2-p180@contactology"
8
+
9
+ #
10
+ # First we attempt to load the desired environment directly from the environment
11
+ # file. This is very fast and efficicent compared to running through the entire
12
+ # CLI and selector. If you want feedback on which environment was used then
13
+ # insert the word 'use' after --create as this triggers verbose mode.
14
+ #
15
+ if [[ -d "${rvm_path:-$HOME/.rvm}/environments" \
16
+ && -s "${rvm_path:-$HOME/.rvm}/environments/$environment_id" ]]
17
+ then
18
+ \. "${rvm_path:-$HOME/.rvm}/environments/$environment_id"
19
+
20
+ if [[ -s ".rvm/hooks/after_use" ]]
21
+ then
22
+ . ".rvm/hooks/after_use"
23
+ fi
24
+ else
25
+ # If the environment file has not yet been created, use the RVM CLI to select.
26
+ if ! rvm --create "$environment_id"
27
+ then
28
+ echo "Failed to create RVM environment 'ruby-1.8.7-p334@contactology'."
29
+ fi
30
+ fi
31
+
32
+ #
33
+ # If you use an RVM gemset file to install a list of gems (*.gems), you can have
34
+ # it be automatically loaded. Uncomment the following and adjust the filename if
35
+ # necessary.
36
+ #
37
+ # filename=".gems"
38
+ # if [[ -s "$filename" ]] ; then
39
+ # rvm gemset import "$filename" | grep -v already | grep -v listed | grep -v complete | sed '/^$/d'
40
+ # fi
41
+
data/.watchr ADDED
@@ -0,0 +1,36 @@
1
+ def run_spec(file)
2
+ unless File.exist?(file)
3
+ puts "#{file} does not exist"
4
+ return
5
+ end
6
+
7
+ puts "Running #{file}"
8
+ system "bundle exec rspec --format documentation #{file}"
9
+ puts
10
+ end
11
+
12
+ def run_all_specs
13
+ puts "Running all specs"
14
+ system "bundle exec rspec spec/"
15
+ puts
16
+ end
17
+
18
+
19
+ watch("spec/.*_spec\.rb") do |match|
20
+ run_spec match[0]
21
+ end
22
+
23
+ watch("lib/(.*)\.rb") do |match|
24
+ run_spec %{spec/models/#{match[1]}_spec.rb}
25
+ end
26
+
27
+ watch("spec/(spec_helper|support/.*)\.rb$") do |match|
28
+ run_all_specs
29
+ end
30
+
31
+
32
+ # Ctrl-\
33
+ Signal.trap 'QUIT' do
34
+ puts
35
+ run_all_specs
36
+ end
data/Gemfile ADDED
@@ -0,0 +1,6 @@
1
+ source 'http://rubygems.org'
2
+ gemspec
3
+
4
+ group :development, :test do
5
+ gem 'webmock', :git => 'https://github.com/bblimke/webmock.git', :ref => 'af4c05480571905be786fc35b8b9703c3ea7fe6c'
6
+ end
data/Rakefile ADDED
@@ -0,0 +1 @@
1
+ require 'bundler/gem_tasks'
@@ -0,0 +1,28 @@
1
+ # -*- encoding: utf-8 -*-
2
+ $:.push File.expand_path('../lib', __FILE__)
3
+ require 'contactology/version'
4
+
5
+ Gem::Specification.new do |s|
6
+ s.name = 'contactology'
7
+ s.version = Contactology::VERSION
8
+ s.authors = ['Nathaniel Bibler']
9
+ s.email = ['git@nathanielbibler.com']
10
+ s.homepage = 'https://github.com/nbibler/contactology'
11
+ s.summary = %q{A Ruby interface to the Contactology email marketing API}
12
+ s.description = %q{This library provides Ruby calls to interact with the Contactology email marketing API}
13
+
14
+ s.files = `git ls-files`.split("\n")
15
+ s.test_files = `git ls-files -- {test,spec,features}/*`.split("\n")
16
+ s.executables = `git ls-files -- bin/*`.split("\n").map{ |f| File.basename(f) }
17
+ s.require_paths = ['lib']
18
+
19
+ s.add_dependency 'hashie', '~> 1.1'
20
+ s.add_dependency 'httparty'
21
+
22
+ s.add_development_dependency 'vcr', '~> 1.5'
23
+ s.add_development_dependency 'rspec', '~> 2.0'
24
+ s.add_development_dependency 'infinity_test'
25
+ s.add_development_dependency 'factory_girl', '~> 2.0'
26
+ s.add_development_dependency 'rack', '~> 1.2'
27
+ s.add_development_dependency 'watchr'
28
+ end
@@ -0,0 +1,125 @@
1
+ require 'contactology/version'
2
+ require 'contactology/errors'
3
+ require 'contactology/campaigns/standard'
4
+ require 'contactology/campaigns/transactional'
5
+ require 'contactology/contact'
6
+ require 'contactology/transactional_message'
7
+ require 'contactology/configuration'
8
+ require 'contactology/list'
9
+ require 'contactology/list_proxy'
10
+
11
+ ##
12
+ # This library provides an interface to the
13
+ # Contactology[http://www.contactology.com/] v2, "REST-based"
14
+ # {email marketing API}[http://www.contactology.com/email-marketing-api/].
15
+ #
16
+ # Example usage:
17
+ #
18
+ # require 'contactology'
19
+ #
20
+ # Contactology.configuration do |config|
21
+ # config.key = 'aBcDeFg12345'
22
+ # end
23
+ #
24
+ # list = Contactology::List.find(4)
25
+ # # => #<Contactology::List:0x000... @list_id="4" @name="test list" ...>
26
+ #
27
+ # list.subscribe('joe@example.local')
28
+ # # => true
29
+ #
30
+ # contact = Contactology::Contact.find('joe@example.local')
31
+ # # => #<Contactology::Contact:0x000... @email="joe@example.local" ...>
32
+ #
33
+ # contact.lists
34
+ # # => [#<Contactology::List:0x000... @list_id="4" ...>]
35
+ #
36
+ module Contactology
37
+ ##
38
+ # Public: Primary accessor for reading or manipulating the default
39
+ # configuration.
40
+ #
41
+ # Examples
42
+ #
43
+ # Contactology.configuration do |config|
44
+ # config.key = 'newkey'
45
+ # end
46
+ #
47
+ # Contactology.configuration
48
+ # # => #<Contactology::Configuration:0x000...>
49
+ #
50
+ # Returns the default Contactology::Configuration instance.
51
+ #
52
+ def self.configuration
53
+ @@_configuration ||= Contactology::Configuration.new
54
+ yield @@_configuration if block_given?
55
+ @@_configuration
56
+ end
57
+
58
+ ##
59
+ # Public: Explicitly sets the default configuration by replacing it with the
60
+ # instance given.
61
+ #
62
+ # configuration - A Contactology::Configuration instance.
63
+ #
64
+ # Examples
65
+ #
66
+ # Contactology.configuration = Contactology::Configuration.new
67
+ # # => #<Contactology::Configuration:0x000...>
68
+ #
69
+ # Returns nothing.
70
+ # Raises ArgumentError unless given a Contactology::Configuration instance.
71
+ #
72
+ def self.configuration=(configuration)
73
+ raise ArgumentError, 'Expected a Contactology::Configuration instance' unless configuration.kind_of?(Configuration)
74
+ @@_configuration = configuration
75
+ end
76
+
77
+ ##
78
+ # Public: Alias to Contactology.configuration.
79
+ #
80
+ # Returns the default Contactology::Configuration instance.
81
+ #
82
+ def self.configure(&block)
83
+ configuration(&block)
84
+ end
85
+
86
+ ##
87
+ # Public: Shorthand reader of the default configuration's endpoint.
88
+ #
89
+ # Returns the String endpoint from the default configuration.
90
+ #
91
+ def self.endpoint
92
+ configuration.endpoint
93
+ end
94
+
95
+ ##
96
+ # Public: Shorthand writer to the default configuration's endpoint.
97
+ #
98
+ # endpoint = The String to use for the API endpoint.
99
+ #
100
+ # Returns nothing.
101
+ #
102
+ def self.endpoint=(endpoint)
103
+ configuration.endpoint = endpoint
104
+ end
105
+
106
+ ##
107
+ # Public: Shorthand reader of the default configuration's key.
108
+ #
109
+ # Returns the String key from the default configuration.
110
+ #
111
+ def self.key
112
+ configuration.key
113
+ end
114
+
115
+ ##
116
+ # Public: Shortand writer to the default configuration's key.
117
+ #
118
+ # key - The String to use for the API key.
119
+ #
120
+ # Returns nothing.
121
+ #
122
+ def self.key=(key)
123
+ configuration.key = key
124
+ end
125
+ end
@@ -0,0 +1,80 @@
1
+ require 'httparty'
2
+ require 'contactology/parser'
3
+
4
+ module Contactology::API
5
+ def query(method, attributes = {})
6
+ configuration = extract_configuration!(attributes)
7
+ handlers = extract_handlers!(attributes)
8
+ remove_empty_values!(attributes)
9
+
10
+ response = HTTParty.get(configuration.endpoint, {
11
+ :query => {:key => configuration.key, :method => method}.merge(attributes),
12
+ :headers => request_headers,
13
+ :parser => request_parser
14
+ })
15
+
16
+ handle_query_response(response, handlers)
17
+ rescue Timeout::Error
18
+ call_response_handler(handlers[:on_timeout], nil)
19
+ end
20
+
21
+ def request_headers
22
+ { 'Accept' => 'application/json', 'User-Agent' => user_agent_string }
23
+ end
24
+
25
+
26
+ protected
27
+
28
+
29
+ def call_response_handler(handler, response)
30
+ unless handler.nil?
31
+ case handler
32
+ when Proc
33
+ handler.call(response)
34
+ when NilClass
35
+ response
36
+ else
37
+ handler
38
+ end
39
+ end
40
+ end
41
+
42
+ def extract_configuration!(hash)
43
+ hash.delete(:configuration) || Contactology.configuration
44
+ end
45
+
46
+ def extract_handlers!(hash)
47
+ handlers = {}
48
+ [:on_timeout, :on_success, :on_error].each { |key| handlers[key] = hash.delete(key) }
49
+ handlers
50
+ end
51
+
52
+ def handle_query_response(response, handlers)
53
+ case
54
+ when response.code == 200
55
+ if response.parsed_response.kind_of?(Hash) &&
56
+ (response['result'] == 'error' || response['success'].kind_of?(FalseClass))
57
+ call_response_handler(handlers[:on_error], response)
58
+ else
59
+ call_response_handler(handlers[:on_success], response)
60
+ end
61
+ else
62
+ call_response_handler(handlers[:on_error], response)
63
+ end
64
+ end
65
+
66
+ def remove_empty_values!(hash)
67
+ hash.each_pair do |k,v|
68
+ hash.delete(k) if v.nil?
69
+ remove_empty_values!(v) if v.kind_of?(Hash)
70
+ end
71
+ end
72
+
73
+ def request_parser
74
+ Contactology::Parser
75
+ end
76
+
77
+ def user_agent_string
78
+ "contactology/#{Contactology::VERSION} (Rubygems; Ruby #{RUBY_VERSION} #{RUBY_PLATFORM})"
79
+ end
80
+ end
@@ -0,0 +1,21 @@
1
+ module Contactology
2
+ if defined? ::BasicObject
3
+ # A class with no predefined methods that behaves similarly to Builder's
4
+ # BlankSlate. Used for proxy classes.
5
+ class BasicObject < ::BasicObject
6
+ undef_method :==
7
+ undef_method :equal?
8
+
9
+ # Let ActiveSupport::BasicObject at least raise exceptions.
10
+ def raise(*args)
11
+ ::Object.send(:raise, *args)
12
+ end
13
+ end
14
+ else
15
+ class BasicObject #:nodoc:
16
+ instance_methods.each do |m|
17
+ undef_method(m) if m.to_s !~ /(?:^__|^nil\?$|^send$|^object_id$)/
18
+ end
19
+ end
20
+ end
21
+ end
@@ -0,0 +1,127 @@
1
+ require 'contactology/stash'
2
+ require 'contactology/api'
3
+ require 'contactology/send_result'
4
+
5
+ module Contactology
6
+ ##
7
+ # Campaigns represent mailing objects on Contactology. These objects may be
8
+ # "standard," meaning that they go out to a List, or "transactional," meaning
9
+ # that they're generated per recipient and used multiple times.
10
+ #
11
+ class Campaign < Contactology::Stash
12
+ autoload :Preview, 'contactology/campaign/preview'
13
+
14
+ extend API
15
+
16
+ property :content
17
+ property :content_type, :from => :contentType
18
+ property :id, :from => :campaignId
19
+ property :name, :from => :campaignName
20
+ property :recipient_name, :from => :recipientName
21
+ property :recipients
22
+ property :reply_to_email, :from => :replyToEmail
23
+ property :reply_to_name, :from => :replyToName
24
+ property :sender_email, :from => :senderEmail
25
+ property :sender_name, :from => :senderName
26
+ property :status
27
+ property :subject
28
+ property :type
29
+ property :start_time, :from => :startTime
30
+ property :authenticate
31
+ property :track_replies, :from => :trackReplies
32
+ property :show_in_archive, :from => :showInArchive
33
+ property :view_in_browser, :from => :viewInBrowser
34
+ property :track_opens, :from => :trackOpens
35
+ property :track_click_thru_html, :from => :trackClickThruHTML
36
+ property :track_click_thru_text, :from => :trackClickThruText
37
+ property :google_analytics_name, :from => :googleAnalyticsName
38
+ property :automatic_tweet, :from => :automaticTweet
39
+ property :click_tale_name, :from => :clickTaleName
40
+ property :click_tale_custom_fields, :from => :clickTaleCustomFields
41
+ property :custom_fields, :from => :customFields
42
+
43
+
44
+ ##
45
+ # Public: Creates a new Campaign on Contactology. This method should not
46
+ # be directly called, but instead called through the Standard or
47
+ # Transactional campaign classes.
48
+ #
49
+ # Returns a campaign instance when successful.
50
+ # Returns false when unsuccessful.
51
+ #
52
+ def self.create(attributes, options = {})
53
+ campaign = new(attributes)
54
+ campaign.save(options) ? campaign : false
55
+ end
56
+
57
+ ##
58
+ # Public: Load the campaign details for a given Contactology Campaign ID.
59
+ #
60
+ # Returns a Contactology::Campaign instance when found.
61
+ # Returns nil for an unrecognized ID or network error.
62
+ #
63
+ def self.find(id, options = {})
64
+ query('Campaign_Get_Info', options.merge({
65
+ 'campaignId' => id,
66
+ :on_success => Proc.new { |response|
67
+ Campaign.new(response) if response.kind_of?(Hash)
68
+ }
69
+ }))
70
+ end
71
+
72
+ def self.find_by_name(name, options = {})
73
+ query('Campaign_Find', options.merge({
74
+ 'searchParameters' => {
75
+ 'campaignName' => name
76
+ },
77
+ :on_success => Proc.new { |response|
78
+ Campaign.new(response.values.first) unless response.nil?
79
+ }
80
+ }))
81
+ end
82
+
83
+
84
+ ##
85
+ # Public: Removes the Campaign from Contactology.
86
+ #
87
+ # Returns true when successful.
88
+ # Returns false when unsuccessful or for a network error.
89
+ #
90
+ def destroy(options = {})
91
+ self.class.query('Campaign_Delete', options.merge({
92
+ 'campaignId' => id,
93
+ :on_timeout => false,
94
+ :on_error => false,
95
+ :on_success => Proc.new { |response| response }
96
+ }))
97
+ end
98
+
99
+ ##
100
+ # Private: This method should be overridden by subclasses of Campaign.
101
+ #
102
+ # Raises NotImplementedError.
103
+ #
104
+ def save(options = {})
105
+ raise NotImplementedError
106
+ end
107
+
108
+ ##
109
+ # Public: Creates the instance on Contactology or raises an exception.
110
+ #
111
+ # Returns the campaign instance when successful.
112
+ # Raises InvalidObjectError when unsuccessful.
113
+ #
114
+ def save!(options = {})
115
+ save(options) || raise(InvalidObjectError)
116
+ end
117
+
118
+ def preview(options = {})
119
+ self.class.query('Campaign_Preview', options.merge({
120
+ 'campaignId' => id,
121
+ :on_error => false,
122
+ :on_timeout => false,
123
+ :on_success => Proc.new { |response| Preview.new(response) }
124
+ }))
125
+ end
126
+ end
127
+ end