contactology 0.0.1

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