active_campaign 0.0.12 → 0.1.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.
@@ -0,0 +1,135 @@
1
+ require 'active_campaign/version'
2
+ require 'active_campaign/response/raise_error'
3
+ require 'active_campaign/response/debugger'
4
+ require 'active_campaign/response/json_normalizer'
5
+ require 'active_campaign/response/mashify'
6
+ require 'active_campaign/response/instrumentation'
7
+ require 'active_campaign/response/parse_json'
8
+
9
+ module ActiveCampaign
10
+
11
+ # Default configuration options for {Client}
12
+ module Default
13
+
14
+ # Default API endpoint
15
+ API_ENDPOINT = "https://subdomain.activehosted.com".freeze
16
+
17
+ API_PATH = "admin/api.php".freeze
18
+
19
+ # Default User Agent header string
20
+ USER_AGENT = "ActiveCampaign Ruby Gem #{ActiveCampaign::VERSION}".freeze
21
+
22
+ # Default media type
23
+ MEDIA_TYPE = "text/html"
24
+
25
+ # Default media type
26
+ API_OUTPUT = "json"
27
+
28
+ # Default WEB endpoint
29
+ WEB_ENDPOINT = "http://www.activecampaign.com/".freeze
30
+
31
+ # Default Faraday middleware stack
32
+ MIDDLEWARE = Faraday::Builder.new do |builder|
33
+ builder.request :url_encoded
34
+ builder.response :mashify
35
+ builder.response :json_normalizer
36
+ builder.response :parse_json
37
+ builder.use :debugger
38
+ builder.use :instrumentation
39
+ builder.adapter Faraday.default_adapter
40
+ end
41
+
42
+ class << self
43
+
44
+ # Configuration options
45
+ # @return [Hash]
46
+ def options
47
+ Hash[ActiveCampaign::Configurable.keys.map{|key| [key, send(key)]}]
48
+ end
49
+
50
+ # Default API endpoint from ENV or {API_ENDPOINT}
51
+ # @return [String]
52
+ def api_endpoint
53
+ ENV['ACTIVE_CAMPAIGN_API_ENDPOINT'] || "#{API_ENDPOINT}/#{api_path}"
54
+ end
55
+
56
+ # Default API endpoint from ENV or {API_ENDPOINT}
57
+ # @return [String]
58
+ def api_path
59
+ API_PATH
60
+ end
61
+
62
+ # Default pagination preference from ENV
63
+ # @return [String]
64
+ def auto_paginate
65
+ ENV['ACTIVE_CAMPAIGN_AUTO_PAGINATE']
66
+ end
67
+
68
+ # Default options for Faraday::Connection
69
+ # @return [Hash]
70
+ def connection_options
71
+ {
72
+ :headers => {
73
+ :accept => default_media_type,
74
+ :user_agent => user_agent
75
+ }
76
+ }
77
+ end
78
+
79
+ # Default media type from ENV or {MEDIA_TYPE}
80
+ # @return [String]
81
+ def default_media_type
82
+ ENV['ACTIVE_CAMPAIGN_DEFAULT_MEDIA_TYPE'] || MEDIA_TYPE
83
+ end
84
+
85
+ # Default GitHub username for Basic Auth from ENV
86
+ # @return [String]
87
+ def api_key
88
+ ENV['ACTIVE_CAMPAIGN_API_KEY']
89
+ end
90
+
91
+ def debug
92
+ false
93
+ end
94
+
95
+ # Default middleware stack for Faraday::Connection
96
+ # from {MIDDLEWARE}
97
+ # @return [String]
98
+ def middleware
99
+ MIDDLEWARE
100
+ end
101
+
102
+ # Default media api_output from ENV or {API_OUTPUT}
103
+ # @return [String]
104
+ def api_output
105
+ ENV['ACTIVE_CAMPAIGN_API_OUTPUT'] || API_OUTPUT
106
+ end
107
+
108
+ # Default pagination page size from ENV
109
+ # @return [Fixnum] Page size
110
+ def per_page
111
+ page_size = ENV['ACTIVE_CAMPAIGN_PER_PAGE']
112
+
113
+ page_size.to_i if page_size
114
+ end
115
+
116
+ # Default proxy server URI for Faraday connection from ENV
117
+ # @return [String]
118
+ def proxy
119
+ ENV['ACTIVE_CAMPAIGN_PROXY']
120
+ end
121
+
122
+ # Default User-Agent header string from ENV or {USER_AGENT}
123
+ # @return [String]
124
+ def user_agent
125
+ ENV['ACTIVE_CAMPAIGN_USER_AGENT'] || USER_AGENT
126
+ end
127
+
128
+ # Default web endpoint from ENV or {WEB_ENDPOINT}
129
+ # @return [String]
130
+ def web_endpoint
131
+ ENV['ACTIVE_CAMPAIGN_WEB_ENDPOINT'] || WEB_ENDPOINT
132
+ end
133
+ end
134
+ end
135
+ end
@@ -1,16 +1,54 @@
1
1
  module ActiveCampaign
2
- # Custom error class for rescuing from all ActiveCampaign errors
2
+ # Custom error class for rescuing from all GitHub errors
3
3
  class Error < StandardError
4
+
5
+ # Returns the appropriate ActiveCampaign::Error sublcass based
6
+ # on status and response message
7
+ #
8
+ # @param [Hash]
9
+ # @returns [ActiveCampaign::Error]
10
+ def self.from_response(response)
11
+ status = response[:status].to_i
12
+ body = response[:body].to_s
13
+
14
+ if klass = case status
15
+ when 400 then ActiveCampaign::BadRequest
16
+ when 401 then ActiveCampaign::Unauthorized
17
+ when 403
18
+ if body =~ /rate limit exceeded/i
19
+ ActiveCampaign::TooManyRequests
20
+ elsif body =~ /login attempts exceeded/i
21
+ ActiveCampaign::TooManyLoginAttempts
22
+ else
23
+ ActiveCampaign::Forbidden
24
+ end
25
+ when 404 then ActiveCampaign::NotFound
26
+ when 406 then ActiveCampaign::NotAcceptable
27
+ when 422 then ActiveCampaign::UnprocessableEntity
28
+ when 500 then ActiveCampaign::InternalServerError
29
+ when 501 then ActiveCampaign::NotImplemented
30
+ when 502 then ActiveCampaign::BadGateway
31
+ when 503 then ActiveCampaign::ServiceUnavailable
32
+ end
33
+ klass.new(response)
34
+ end
35
+ end
36
+
4
37
  def initialize(response=nil)
5
38
  @response = response
6
39
  super(build_error_message)
7
40
  end
8
41
 
9
- def response_body
10
- @response_body ||=
42
+ private
43
+
44
+ def data
45
+ @data ||=
11
46
  if (body = @response[:body]) && !body.empty?
12
- if body.is_a?(String)
13
- MultiJson.load(body, :symbolize_keys => true)
47
+ if body.is_a?(String) &&
48
+ @response[:response_headers] &&
49
+ @response[:response_headers][:content_type] =~ /json/
50
+
51
+ Sawyer::Agent.serializer.decode(body)
14
52
  else
15
53
  body
16
54
  end
@@ -19,50 +57,78 @@ module ActiveCampaign
19
57
  end
20
58
  end
21
59
 
22
- private
60
+ def response_message
61
+ case data
62
+ when Hash
63
+ data[:message]
64
+ when String
65
+ data
66
+ end
67
+ end
68
+
69
+ def response_error
70
+ "Error: #{data[:error]}" if data.is_a?(Hash) && data[:error]
71
+ end
72
+
73
+ def response_error_summary
74
+ return nil unless data.is_a?(Hash) && !Array(data[:errors]).empty?
75
+
76
+ summary = "\nError summary:\n"
77
+ summary << data[:errors].map do |hash|
78
+ hash.map { |k,v| " #{k}: #{v}" }
79
+ end.join("\n")
80
+
81
+ summary
82
+ end
23
83
 
24
84
  def build_error_message
25
- return nil if @response.nil?
85
+ return nil if @response.nil?
26
86
 
27
- message = if response_body
28
- ": #{response_body[:error] || response_body[:message] || ''}"
29
- else
30
- ''
31
- end
32
- errors = unless message.empty?
33
- response_body[:errors] ? ": #{response_body[:errors].map{|e|e[:message]}.join(', ')}" : ''
34
- end
35
- "#{@response[:method].to_s.upcase} #{@response[:url].to_s}: #{@response[:status]}#{message}#{errors}"
87
+ message = "#{@response[:method].to_s.upcase} "
88
+ message << "#{@response[:url].to_s}: "
89
+ message << "#{@response[:status]} - "
90
+ message << "#{response_message}" unless response_message.nil?
91
+ message << "#{response_error}" unless response_error.nil?
92
+ message << "#{response_error_summary}" unless response_error_summary.nil?
93
+ message
36
94
  end
37
95
  end
38
96
 
39
- # Raised when ActiveCampaign returns a 400 HTTP status code
97
+ # Raised when GitHub returns a 400 HTTP status code
40
98
  class BadRequest < Error; end
41
99
 
42
- # Raised when ActiveCampaign returns a 401 HTTP status code
100
+ # Raised when GitHub returns a 401 HTTP status code
43
101
  class Unauthorized < Error; end
44
102
 
45
- # Raised when ActiveCampaign returns a 403 HTTP status code
103
+ # Raised when GitHub returns a 403 HTTP status code
46
104
  class Forbidden < Error; end
47
105
 
48
- # Raised when ActiveCampaign returns a 404 HTTP status code
106
+ # Raised when GitHub returns a 403 HTTP status code
107
+ # and body matches 'rate limit exceeded'
108
+ class TooManyRequests < Forbidden; end
109
+
110
+ # Raised when GitHub returns a 403 HTTP status code
111
+ # and body matches 'login attempts exceeded'
112
+ class TooManyLoginAttempts < Forbidden; end
113
+
114
+ # Raised when GitHub returns a 404 HTTP status code
49
115
  class NotFound < Error; end
50
116
 
51
- # Raised when ActiveCampaign returns a 406 HTTP status code
117
+ # Raised when GitHub returns a 406 HTTP status code
52
118
  class NotAcceptable < Error; end
53
119
 
54
- # Raised when ActiveCampaign returns a 422 HTTP status code
120
+ # Raised when GitHub returns a 422 HTTP status code
55
121
  class UnprocessableEntity < Error; end
56
122
 
57
- # Raised when ActiveCampaign returns a 500 HTTP status code
123
+ # Raised when GitHub returns a 500 HTTP status code
58
124
  class InternalServerError < Error; end
59
125
 
60
- # Raised when ActiveCampaign returns a 501 HTTP status code
126
+ # Raised when GitHub returns a 501 HTTP status code
61
127
  class NotImplemented < Error; end
62
128
 
63
- # Raised when ActiveCampaign returns a 502 HTTP status code
129
+ # Raised when GitHub returns a 502 HTTP status code
64
130
  class BadGateway < Error; end
65
131
 
66
- # Raised when ActiveCampaign returns a 503 HTTP status code
132
+ # Raised when GitHub returns a 503 HTTP status code
67
133
  class ServiceUnavailable < Error; end
68
- end
134
+ end
@@ -0,0 +1,42 @@
1
+ require 'faraday'
2
+ require 'forwardable'
3
+
4
+ module ActiveCampaign
5
+ module Response
6
+ class Debugger < Faraday::Response::Middleware
7
+ extend Forwardable
8
+
9
+ def initialize(app, logger = nil)
10
+ super(app)
11
+ @logger = logger || begin
12
+ require 'logger'
13
+ ::Logger.new(STDOUT)
14
+ end
15
+ end
16
+
17
+ def_delegators :@logger, :debug, :info, :warn, :error, :fatal
18
+
19
+ def call(env)
20
+ info("#{env[:method]} #{env[:url].to_s}")
21
+ debug('request') { dump_headers(env[:request_headers]) + "\nbody: #{env[:body]}" }
22
+ super
23
+ end
24
+
25
+ def on_complete(env)
26
+ info('Status') { env[:status].to_s }
27
+ debug('response-head') { dump_headers env[:response_headers] }
28
+ debug('response-body') { env[:body] }
29
+ end
30
+
31
+ private
32
+
33
+ def dump_headers(headers)
34
+ headers.map { |k, v| "#{k}: #{v.inspect}" }.join("\n")
35
+ end
36
+ end
37
+ end
38
+ end
39
+
40
+ Faraday.register_middleware :middleware, debugger: lambda {
41
+ ActiveCampaign::Response::Debugger
42
+ }
@@ -0,0 +1,33 @@
1
+ module ActiveCampaign
2
+ module Response
3
+ # Public: Instruments requests using Active Support.
4
+ #
5
+ # Measures time spent only for synchronous requests.
6
+ #
7
+ # Examples
8
+ #
9
+ # ActiveSupport::Notifications.subscribe('request.faraday') do |name, starts, ends, _, env|
10
+ # url = env[:url]
11
+ # http_method = env[:method].to_s.upcase
12
+ # duration = ends - starts
13
+ # $stderr.puts '[%s] %s %s (%.3f s)' % [url.host, http_method, url.request_uri, duration]
14
+ # end
15
+ class Instrumentation < Faraday::Middleware
16
+ dependency 'active_support/notifications'
17
+
18
+ def initialize(app, options = {})
19
+ super(app)
20
+ @name = options.fetch(:name, 'request.faraday')
21
+ end
22
+
23
+ def call(env)
24
+ ActiveSupport::Notifications.instrument(@name, env) do
25
+ @app.call(env)
26
+ end
27
+ end
28
+ end
29
+ end
30
+ end
31
+ Faraday.register_middleware :middleware, instrumentation: lambda {
32
+ ActiveCampaign::Response::Instrumentation
33
+ }
@@ -0,0 +1,49 @@
1
+ require 'faraday'
2
+ require 'faraday/response'
3
+
4
+ module ActiveCampaign
5
+ module Response
6
+ class JsonNormalizer < Faraday::Response::Middleware
7
+ def initialize(app, logger = nil)
8
+ super(app)
9
+ end
10
+
11
+ def call(environment)
12
+ @app.call(environment).on_complete do |env|
13
+ if env[:body].is_a?(Hash)
14
+ env[:body] = normalize(env[:body])
15
+ end
16
+ end
17
+ end
18
+
19
+ private
20
+
21
+ def normalize(response)
22
+ keys, values = keys_values(response)
23
+
24
+ if keys.all?{|key| is_numeric?(key) }
25
+ response[:results] = values
26
+ keys.each do |key|
27
+ response.delete(key)
28
+ end
29
+ end
30
+
31
+ response
32
+ end
33
+
34
+ def is_numeric?(string)
35
+ string.to_s.match(/\A[+-]?\d+\Z/) == nil ? false : true
36
+ end
37
+
38
+ def keys_values(response)
39
+ results = results(response)
40
+ [results.keys, results.values]
41
+ end
42
+
43
+ def results(response)
44
+ response.reject{|k,v| %w(result_code result_message result_output).include?(k) }
45
+ end
46
+ end
47
+ end
48
+ end
49
+ Faraday.register_middleware :response, json_normalizer: lambda { ActiveCampaign::Response::JsonNormalizer }
@@ -0,0 +1,40 @@
1
+ require 'faraday'
2
+
3
+ module ActiveCampaign
4
+ module Response
5
+
6
+ # Public: Converts parsed response bodies to a Hashie::Mash if they were of
7
+ # Hash or Array type.
8
+ class Mashify < Faraday::Response::Middleware
9
+ attr_accessor :mash_class
10
+
11
+ class << self
12
+ attr_accessor :mash_class
13
+ end
14
+
15
+ dependency do
16
+ require 'hashie/mash'
17
+ self.mash_class = ::Hashie::Mash
18
+ end
19
+
20
+ def initialize(app = nil, options = {})
21
+ super(app)
22
+ self.mash_class = options[:mash_class] || self.class.mash_class
23
+ end
24
+
25
+ def parse(body)
26
+ case body
27
+ when Hash
28
+ mash_class.new(body)
29
+ when Array
30
+ body.map { |item| parse(item) }
31
+ else
32
+ body
33
+ end
34
+ end
35
+ end
36
+ end
37
+ end
38
+ Faraday.register_middleware :response, mashify: lambda {
39
+ ActiveCampaign::Response::Mashify
40
+ }