mailchimp-rest-api 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.
checksums.yaml ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA256:
3
+ metadata.gz: f7356505754644055f03ee2a25a23910247fc87fb350672e6a1fa3f3ead64626
4
+ data.tar.gz: f95ac89a0685693ed4be9afb6f5d78fb2948b724b96ac7d7a130e151ae275573
5
+ SHA512:
6
+ metadata.gz: d40b3c60e6ab469604cbca9c7d10da2a91483e9c9653d461d5c9485c84243162b2996ce17e1466607d25ae970a6da3d2075dcbe39c870b6c13ed5f12603444b6
7
+ data.tar.gz: ace08ae4987164126a36da7879ae75a7f4acb571b2f1ca8b61df51a5788410636b5c7d105bae535e51e973f21b67984aece570b221e9f951a1d03f0f400c0bb8
data/README.md ADDED
@@ -0,0 +1,154 @@
1
+ # Mailchimp REST API (Marketing)
2
+
3
+ ## Installation
4
+
5
+ ```bash
6
+ bundle add mailchimp-rest-api
7
+ ```
8
+
9
+ ## Features
10
+
11
+ - Supported Ruby Versions - *(2.6 .. 3.3), head, jruby-9.4, truffleruby-24*
12
+ - No dependencies;
13
+ - Auto-retries (configured);
14
+ - Pagination methods
15
+
16
+ ## Usage
17
+
18
+ ```ruby
19
+ # Setup client
20
+ MailchimpAPI.client = MailchimpAPI::Client.new(api_key: ENV['MAILCHIMP_API_KEY'])
21
+
22
+ # Call any HTTP API
23
+ MailchimpAPI.post(path, query: query, body: body, headers: headers)
24
+ MailchimpAPI.get(path, query: query, body: body, headers: headers)
25
+ MailchimpAPI.patch(path, query: query, body: body, headers: headers)
26
+ MailchimpAPI.put(path, query: query, body: body, headers: headers)
27
+ MailchimpAPI.delete(path, query: query, body: body, headers: headers)
28
+
29
+ # Call any defined REST API (defined in lib/resources)
30
+ MailchimpAPI::Audience::Members.show(audience_id, email, query: query)
31
+ MailchimpAPI::Audience::Members.create(audience_id, body: body)
32
+
33
+ # Or call it via shortcuts (defined in lib/client/api_methods.rb)
34
+ MailchimpAPI.audience_members.show(audience_id, email, query: query)
35
+ MailchimpAPI.audience_members.create(audience_id, body: body)
36
+ ```
37
+
38
+ ### Response
39
+
40
+ `Response` object is returned after each `API` request.
41
+
42
+ #### Original HTTP response data
43
+
44
+ - `response.http_status` - response HTTP status as Integer
45
+ - `response.http_body` - response body as String
46
+ - `response.http_headers` - response headers as Hash with String keys
47
+ - `response.http_response` - original Net::HTTP::Response object
48
+ - `response.request` - Request object that was used to get this response
49
+
50
+ #### Parsed JSON body methods
51
+
52
+ - `response.body` - parsed JSON body, keys are Symbols
53
+ - `response[:field]` - gets `:field` attribute from parsed body,
54
+ returns nil if response have no such key
55
+ - `response.fetch(:field)` - gets `:field` attribute from parsed body,
56
+ raises KeyError if response has no such key
57
+
58
+ #### Error check methods
59
+
60
+ - `response.success?` - checks HTTP status code is 2xx
61
+ - `response.failed?` - checks HTTP status code is not 2xx
62
+
63
+ ## Configuration options
64
+
65
+ MailchimpAPI client accepts this additional options:
66
+
67
+ - `:retries`
68
+ - `:http_opts`
69
+
70
+ ### Option `:retries`
71
+
72
+ This is a Hash with retries configuration.
73
+ By default retries are enabled, 4 retries with 0, 0.25, 0.75, 1.5 seconds delay.
74
+ Default config: `{enabled: true, count: 4, sleep: [0, 0.25, 0.75, 1.5]}`.
75
+ New options are merged with defaults.
76
+ Please keep `sleep` array same size as `count`.
77
+
78
+ Retries happen on any network error, on 409, 429, 5xx response status code.
79
+
80
+ ```ruby
81
+ client = MailchimpAPI::Client.new(
82
+ retries: {enabled: !Rails.env.test?, count: 5, sleep: [0, 0.25, 0.75, 1.5, 2]}
83
+ # ...
84
+ )
85
+ ```
86
+
87
+ ### Option `:http_opts`
88
+
89
+ This are the options that are provided to the `Net::HTTP.start` method,
90
+ like `:read_timeout`, `:write_timeout`, etc.
91
+
92
+ You can find full list of available options here <https://docs.ruby-lang.org/en/master/Net/HTTP.html#method-c-start>
93
+ (Please choose you version of ruby).
94
+
95
+ By default it is an empty hash.
96
+
97
+ ```ruby
98
+ client = MailchimpAPI::Client.new(
99
+ http_opts: {read_timeout: 30, write_timeout: 30, open_timeout: 30}
100
+ # ...
101
+ )
102
+ ```
103
+
104
+ ## Pagination
105
+
106
+ We have two specific methods:
107
+
108
+ - `MailchimpAPI#each_page(response)` - iterates over current and each next page
109
+ - `MailchimpAPI#each_page_item(response, items_field_name)` - iterates over
110
+ items on each page
111
+
112
+ Example:
113
+
114
+ ```ruby
115
+ first_page = MailchimpAPI.audience_members.list(list_id, query: { count: 100 }
116
+
117
+ MailchimpAPI.each_page(first_page) do |response|
118
+ puts response.body
119
+ end
120
+
121
+ MailchimpAPI.each_page_item(first_page, :members) do |item|
122
+ puts item[:email_address]
123
+ end
124
+ ```
125
+
126
+ ## Request
127
+
128
+ ...
129
+
130
+ ## Errors
131
+
132
+ ...
133
+
134
+ ## APIs
135
+
136
+ ...
137
+
138
+ ## Development
139
+
140
+ ```bash
141
+ rubocop
142
+ rspec
143
+ mdl README.md CHANGELOG.md RELEASE.md
144
+ ```
145
+
146
+ ## Contributing
147
+
148
+ Bug reports and pull requests are welcome on GitHub at <https://github.com/aglushkov/mailchimp-rest-api>.
149
+
150
+ ## License
151
+
152
+ The gem is available as open source under the terms of the [MIT License](https://opensource.org/licenses/MIT).
153
+
154
+ [pagination]: #pagination
data/VERSION ADDED
@@ -0,0 +1 @@
1
+ 0.0.1
@@ -0,0 +1,32 @@
1
+ # frozen_string_literal: true
2
+
3
+ module MailchimpAPI
4
+ class BatchRequest
5
+ API_VERSION_PREFIX = "/#{MailchimpAPI::API_VERSION}"
6
+
7
+ attr_reader :request, :operation_id
8
+
9
+ def initialize(request, operation_id: nil)
10
+ @request = request
11
+ @operation_id = operation_id
12
+ end
13
+
14
+ # Make a batch operation request
15
+ # https://mailchimp.com/developer/marketing/guides/run-async-requests-batch-endpoint/#make-a-batch-operations-request
16
+ def operation
17
+ operation = {
18
+ method: request.method, # "GET", "POST", "PUT", "PATCH", "DELETE"
19
+ path: request.path.delete_prefix(API_VERSION_PREFIX) # Path relative to version prefix
20
+ }
21
+
22
+ body = request.body
23
+ operation[:body] = body if body
24
+
25
+ params = request.query
26
+ operation[:params] = params unless params.empty?
27
+
28
+ operation[:operation_id] = operation_id if operation_id
29
+ operation
30
+ end
31
+ end
32
+ end
@@ -0,0 +1,18 @@
1
+ # frozen_string_literal: true
2
+
3
+ module MailchimpAPI
4
+ class Client
5
+ #
6
+ # Methods to access API resources
7
+ #
8
+ module APIMethods
9
+ def audience_members
10
+ Audience::Members.new(self)
11
+ end
12
+
13
+ def audience_member_tags
14
+ Audience::MemberTags.new(self)
15
+ end
16
+ end
17
+ end
18
+ end
@@ -0,0 +1,44 @@
1
+ # frozen_string_literal: true
2
+
3
+ module MailchimpAPI
4
+ class Client
5
+ #
6
+ # Methods to create and run batch requests
7
+ #
8
+ module BatchMethods
9
+ def batch_get_request(path, query: nil, body: nil, headers: nil, operation_id: nil)
10
+ new_batch_request(Net::HTTP::Get, path, query: query, body: body, headers: headers, operation_id: operation_id)
11
+ end
12
+
13
+ def batch_post_request(path, query: nil, body: nil, headers: nil, operation_id: nil)
14
+ new_batch_request(Net::HTTP::Post, path, query: query, body: body, headers: headers, operation_id: operation_id)
15
+ end
16
+
17
+ def batch_put_request(path, query: nil, body: nil, headers: nil, operation_id: nil)
18
+ new_batch_request(Net::HTTP::Put, path, query: query, body: body, headers: headers, operation_id: operation_id)
19
+ end
20
+
21
+ def batch_patch_request(path, query: nil, body: nil, headers: nil, operation_id: nil)
22
+ new_batch_request(Net::HTTP::Patch, path, query: query, body: body, headers: headers, operation_id: operation_id)
23
+ end
24
+
25
+ def batch_delete_request(path, query: nil, body: nil, headers: nil, operation_id: nil)
26
+ new_batch_request(Net::HTTP::Delete, path, query: query, body: body, headers: headers, operation_id: operation_id)
27
+ end
28
+
29
+ def batch(batch_requests, query: nil)
30
+ operations = batch_requests.map(&:operation)
31
+ post("batches", query: query, body: {operations: operations})
32
+ end
33
+
34
+ private
35
+
36
+ # rubocop:disable Metrics/ParameterLists
37
+ def new_batch_request(http_method, path, query: nil, body: nil, headers: nil, operation_id: nil)
38
+ request = new_request(http_method, path: path, query: query, body: body, headers: headers)
39
+ BatchRequest.new(request, operation_id: operation_id)
40
+ end
41
+ # rubocop:enable Metrics/ParameterLists
42
+ end
43
+ end
44
+ end
@@ -0,0 +1,51 @@
1
+ # frozen_string_literal: true
2
+
3
+ module MailchimpAPI
4
+ class Client
5
+ module PaginationMethods
6
+ # rubocop:disable Metrics/AbcSize, Metrics/MethodLength
7
+ def next_page(response)
8
+ unless response.body.key?(:total_items)
9
+ raise Error, "Please ensure `total_items` field is not excluded from response"
10
+ end
11
+
12
+ request = response.request
13
+ query = request.query
14
+
15
+ count = query.key?(:count) ? query[:count].to_i : 10
16
+ offset = query.key?(:offset) ? query[:offset].to_i : 0
17
+
18
+ body = response.body
19
+ total_items = body[:total_items].to_i
20
+ is_final_page = ((offset + count) >= total_items)
21
+ return if is_final_page
22
+
23
+ new_query = query.merge(offset: offset + count, count: count)
24
+
25
+ get(
26
+ request.path,
27
+ query: new_query,
28
+ body: request.body,
29
+ headers: request.headers
30
+ )
31
+ end
32
+ # rubocop:enable Metrics/AbcSize, Metrics/MethodLength
33
+
34
+ def each_page(response, &block)
35
+ return enum_for(:each_page, response) unless block
36
+
37
+ page = response
38
+ yield(page)
39
+ yield(page) while (page = next_page(page))
40
+ end
41
+
42
+ def each_page_item(response, items_field_name, &block)
43
+ return enum_for(:each_page_item, response, items_field_name) unless block
44
+
45
+ each_page(response) do |page|
46
+ page.body.fetch(items_field_name).each(&block)
47
+ end
48
+ end
49
+ end
50
+ end
51
+ end
@@ -0,0 +1,55 @@
1
+ # frozen_string_literal: true
2
+
3
+ module MailchimpAPI
4
+ class Client
5
+ API_VERSION = MailchimpAPI::API_VERSION
6
+
7
+ include APIMethods
8
+ include BatchMethods
9
+ include PaginationMethods
10
+
11
+ attr_reader :api_key, :api_url, :api_version, :authorization_token, :config
12
+
13
+ def initialize(api_key:, http_opts: nil, retries: nil)
14
+ raise ArgumentError, "Invalid api_key" unless /\w+-\w+/.match?(api_key) # <token>-<dc>
15
+ dc = api_key.split("-", 2).last # initial api_key must have format
16
+
17
+ @api_key = api_key
18
+ @api_url = "https://#{dc}.api.mailchimp.com/#{API_VERSION}/"
19
+ @authorization_token = "Bearer #{api_key}"
20
+ @config = Config.new(http_opts: http_opts, retries: retries)
21
+ end
22
+
23
+ def post(path, query: nil, body: nil, headers: nil)
24
+ execute(Net::HTTP::Post, path, query: query, body: body, headers: headers)
25
+ end
26
+
27
+ def get(path, query: nil, body: nil, headers: nil)
28
+ execute(Net::HTTP::Get, path, query: query, body: body, headers: headers)
29
+ end
30
+
31
+ def patch(path, query: nil, body: nil, headers: nil)
32
+ execute(Net::HTTP::Patch, path, query: query, body: body, headers: headers)
33
+ end
34
+
35
+ def put(path, query: nil, body: nil, headers: nil)
36
+ execute(Net::HTTP::Put, path, query: query, body: body, headers: headers)
37
+ end
38
+
39
+ def delete(path, query: nil, body: nil, headers: nil)
40
+ execute(Net::HTTP::Delete, path, query: query, body: body, headers: headers)
41
+ end
42
+
43
+ private
44
+
45
+ def execute(http_method, path, query:, body:, headers:)
46
+ headers = {"authorization" => authorization_token}.merge!(headers || {})
47
+ request = new_request(http_method, path: path, query: query, body: body, headers: headers)
48
+ RequestExecutor.new(request, http_opts: config.http_opts, retries: config.retries).call
49
+ end
50
+
51
+ def new_request(http_method, path:, query:, body:, headers:)
52
+ Request.new(http_method, url: api_url, path: path, query: query, body: body, headers: headers)
53
+ end
54
+ end
55
+ end
@@ -0,0 +1,30 @@
1
+ # frozen_string_literal: true
2
+
3
+ module MailchimpAPI
4
+ #
5
+ # Stores client requests configuration
6
+ #
7
+ class Config
8
+ # Default config options
9
+ DEFAULTS = {
10
+ http_opts: {}.freeze,
11
+ retries: {enabled: true, count: 4, sleep: [0, 0.25, 0.75, 1.5].freeze}.freeze
12
+ }.freeze
13
+
14
+ attr_reader :http_opts, :retries
15
+
16
+ # Initializes Config
17
+ #
18
+ # @param http_opts [Hash] Net::Http opts for all requests
19
+ # @param retries [Hash] Retries configuration
20
+ #
21
+ # @return [Client] Initialized config object
22
+ #
23
+ def initialize(http_opts: nil, retries: {})
24
+ @http_opts = http_opts.dup.freeze || DEFAULTS[:http_opts]
25
+ @retries = DEFAULTS[:retries].merge(retries || {}).freeze
26
+
27
+ freeze
28
+ end
29
+ end
30
+ end
@@ -0,0 +1,118 @@
1
+ # frozen_string_literal: true
2
+
3
+ module MailchimpAPI
4
+ #
5
+ # Common interface for all errors
6
+ #
7
+ class Error < StandardError
8
+ # @return [Response, nil] Returned response with non-200 status code
9
+ attr_reader :response
10
+
11
+ # @return [Request] Sent request
12
+ attr_reader :request
13
+
14
+ # @return [String, nil] Error type returned by Mailchimp
15
+ attr_reader :error_type
16
+
17
+ # @return [String] Error title returned by Mailchimp or response error class name
18
+ attr_reader :error_title
19
+
20
+ # @return [String] Error status returned by Mailchimp or response HTTP status
21
+ attr_reader :error_status
22
+
23
+ # @return [String, nil] Error description returned by Mailchimp
24
+ attr_reader :error_detail
25
+
26
+ # @return [String, nil] Error ID returned by Mailchimp
27
+ attr_reader :error_instance
28
+
29
+ # @return [Hash, nil] Fields specific errors returned by Mailchimp
30
+ attr_reader :error_fields
31
+ end
32
+
33
+ #
34
+ # Namespace for specific MailchimpAPI errors
35
+ #
36
+ module Errors
37
+ #
38
+ # Raised when Mailchimp responds with any status code except 200, 201, 202, 204
39
+ #
40
+ class FailedRequest < Error
41
+ def initialize(message = nil, request:, response:)
42
+ @request = request
43
+ @response = response
44
+
45
+ body = response.body
46
+ data = body.is_a?(Hash) ? body : {}
47
+ @error_type = data[:type]
48
+ @error_title = data[:title] || response.http_response.class.name
49
+ @error_detail = data[:detail]
50
+ @error_status = data[:status] || response.http_status
51
+ @error_fields = data[:errors]
52
+ @error_instance = data[:instance]
53
+
54
+ message += "\n #{response.http_body}" unless data.empty?
55
+ super(message)
56
+ end
57
+ end
58
+
59
+ #
60
+ # Raised when a network raised when executing the request
61
+ # List of network errors can be found in errors/network_error_builder.rb
62
+ #
63
+ class NetworkError < Error
64
+ def initialize(message = nil, request:, error:)
65
+ super(message)
66
+ @request = request
67
+ @response = nil
68
+
69
+ @error_type = nil
70
+ @error_title = error.class.name
71
+ @error_detail = error.message
72
+ @error_status = nil
73
+ @error_fields = nil
74
+ @error_instance = nil
75
+ end
76
+ end
77
+
78
+ # 400
79
+ class BadRequest < FailedRequest
80
+ end
81
+
82
+ # 401
83
+ class Unauthorized < FailedRequest
84
+ end
85
+
86
+ # 403
87
+ class Forbidden < FailedRequest
88
+ end
89
+
90
+ # 404
91
+ class NotFound < FailedRequest
92
+ end
93
+
94
+ # 405
95
+ class MethodNotAllowed < FailedRequest
96
+ end
97
+
98
+ # 414
99
+ class RequestURITooLong < FailedRequest
100
+ end
101
+
102
+ # 422
103
+ class UnprocessableEntity < FailedRequest
104
+ end
105
+
106
+ # 426
107
+ class UpgradeRequired < FailedRequest
108
+ end
109
+
110
+ # 429
111
+ class TooManyRequests < FailedRequest
112
+ end
113
+
114
+ # 5xx
115
+ class ServerError < FailedRequest
116
+ end
117
+ end
118
+ end
@@ -0,0 +1,48 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "net/http"
4
+
5
+ module MailchimpAPI
6
+ #
7
+ # Builds MailchimpAPI::FailedRequest error
8
+ #
9
+ class FailedRequestErrorBuilder
10
+ # Matchings for Net::HTTP response class to MailchimpAPI::Error class
11
+ RESPONSE_ERROR_MAP = {
12
+ Net::HTTPBadRequest => Errors::BadRequest, # 400
13
+ Net::HTTPUnauthorized => Errors::Unauthorized, # 401
14
+ Net::HTTPForbidden => Errors::Forbidden, # 403
15
+ Net::HTTPNotFound => Errors::NotFound, # 404
16
+ Net::HTTPMethodNotAllowed => Errors::MethodNotAllowed, # 405
17
+ Net::HTTPRequestURITooLong => Errors::RequestURITooLong, # 414
18
+ Net::HTTPUnprocessableEntity => Errors::UnprocessableEntity, # 422
19
+ Net::HTTPUpgradeRequired => Errors::UpgradeRequired, # 426
20
+ Net::HTTPTooManyRequests => Errors::TooManyRequests, # 429
21
+ Net::HTTPServerError => Errors::ServerError # 5xx
22
+ }.freeze
23
+
24
+ class << self
25
+ # Builds FailedRequestError instance
26
+ #
27
+ # @param request [Request] Original request
28
+ # @param response [Response] Original response
29
+ #
30
+ # @return [Errors::FailedRequestError] error object
31
+ #
32
+ def call(request:, response:)
33
+ http_response = response.http_response
34
+ error_message = "#{http_response.code} #{http_response.message}"
35
+ error_class = find_error_class(http_response)
36
+
37
+ error_class.new(error_message, response: response, request: request)
38
+ end
39
+
40
+ private
41
+
42
+ def find_error_class(http_response)
43
+ found_class = RESPONSE_ERROR_MAP.keys.find { |http_error_class| http_response.is_a?(http_error_class) }
44
+ found_class ? RESPONSE_ERROR_MAP[found_class] : Errors::FailedRequest
45
+ end
46
+ end
47
+ end
48
+ end
@@ -0,0 +1,35 @@
1
+ # frozen_string_literal: true
2
+
3
+ module MailchimpAPI
4
+ #
5
+ # Builds MailchimpAPI::Errors::NetowrkError error
6
+ #
7
+ class NetworkErrorBuilder
8
+ # List of possible Network errors
9
+ ERRORS = [
10
+ IOError,
11
+ Errno::ECONNABORTED,
12
+ Errno::ECONNREFUSED,
13
+ Errno::ECONNRESET,
14
+ Errno::EHOSTUNREACH,
15
+ Errno::EPIPE,
16
+ Errno::ETIMEDOUT,
17
+ OpenSSL::SSL::SSLError,
18
+ SocketError,
19
+ Timeout::Error # Net::OpenTimeout, Net::ReadTimeout
20
+ ].freeze
21
+
22
+ class << self
23
+ # Builds NetworkError instance
24
+ #
25
+ # @param request [Request] Original request
26
+ # @param error [StandardError] Original error
27
+ #
28
+ # @return [Errors::NetworkError] Built NetworkError
29
+ #
30
+ def call(request:, error:)
31
+ Errors::NetworkError.new(error.message, request: request, error: error)
32
+ end
33
+ end
34
+ end
35
+ end
@@ -0,0 +1,93 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "json"
4
+
5
+ module MailchimpAPI
6
+ class Request
7
+ attr_reader \
8
+ :init_http_method,
9
+ :init_base_url,
10
+ :init_path,
11
+ :init_query,
12
+ :init_body,
13
+ :init_headers,
14
+ :http_request
15
+
16
+ # rubocop:disable Metrics/ParameterLists, Metrics/MethodLength
17
+ def initialize(http_method, url:, path:, query: nil, body: nil, headers: nil)
18
+ @init_http_method = http_method
19
+ @init_url = url
20
+ @init_path = path
21
+ @init_query = query
22
+ @init_body = body
23
+ @init_headers = headers
24
+
25
+ uri = prepare_uri(url, path, query)
26
+ headers = prepare_headers(headers)
27
+ body = prepare_body(body)
28
+ http_method = prepare_http_method(http_method, headers)
29
+
30
+ @http_request = build_http_request(http_method, uri: uri, body: body, headers: headers)
31
+ end
32
+ # rubocop:enable Metrics/ParameterLists, Metrics/MethodLength
33
+
34
+ # @return [String] HTTP request method name
35
+ def method
36
+ http_request.method # "GET", "POST", "PUT", "PATCH", "DELETE"
37
+ end
38
+
39
+ # @return [String] HTTP request full path
40
+ def path
41
+ http_request.path
42
+ end
43
+
44
+ # @return [URI] HTTP request URI
45
+ def uri
46
+ http_request.uri
47
+ end
48
+
49
+ # @return [String, nil] HTTP request body
50
+ def body
51
+ http_request.body
52
+ end
53
+
54
+ # @return [Hash] HTTP request query params
55
+ def query
56
+ @query ||= uri.query ? URI.decode_www_form(uri.query).to_h.transform_keys!(&:to_sym) : {}
57
+ end
58
+
59
+ # @return [Hash] HTTP request headers
60
+ def headers
61
+ http_request.each_header.to_h
62
+ end
63
+
64
+ private
65
+
66
+ def build_http_request(http_method, uri:, body:, headers:)
67
+ http_request = http_method.new(uri, "accept-encoding" => nil, "content-type" => "application/json")
68
+
69
+ headers.each { |key, value| http_request[key] = value }
70
+ http_request.body = body
71
+
72
+ http_request
73
+ end
74
+
75
+ def prepare_uri(url, path, query)
76
+ URIBuilder.call(url: url, path: path, query: query)
77
+ end
78
+
79
+ def prepare_headers(headers)
80
+ return {} unless headers
81
+
82
+ headers.transform_keys { |key| key.to_s.downcase }
83
+ end
84
+
85
+ def prepare_body(body)
86
+ JSON.dump(body) unless body.nil?
87
+ end
88
+
89
+ def prepare_http_method(default_http_method, headers)
90
+ headers["x-http-method-override"] ? Net::HTTP::Post : default_http_method
91
+ end
92
+ end
93
+ end
@@ -0,0 +1,84 @@
1
+ # frozen_string_literal: true
2
+
3
+ module MailchimpAPI
4
+ class RequestExecutor
5
+ attr_reader :request, :http_opts, :retries
6
+
7
+ def initialize(request, http_opts: {}, retries: {})
8
+ @request = request
9
+ @http_opts = http_opts
10
+ @retries = retries
11
+ end
12
+
13
+ #
14
+ # @return [Response] Response
15
+ #
16
+ def call
17
+ response = execute_request
18
+ raise FailedRequestErrorBuilder.call(request: request, response: response) if response.failed?
19
+
20
+ response
21
+ end
22
+
23
+ private
24
+
25
+ def execute_request(retry_number: 0)
26
+ response = execute_net_http_request
27
+ rescue => error
28
+ unknown_network_error?(error) ? handle_unknown_error(error) : handle_network_error(error, retry_number)
29
+ else
30
+ response.success? ? response : handle_failed_response(response, retry_number)
31
+ end
32
+
33
+ def handle_network_error(error, retry_number)
34
+ will_retry = retries[:enabled] && !retries_limit_reached?(retry_number)
35
+ raise NetworkErrorBuilder.call(request: request, error: error) unless will_retry
36
+
37
+ retry_request(retry_number)
38
+ end
39
+
40
+ def handle_failed_response(response, retry_number)
41
+ will_retry = retries[:enabled] && retryable?(response, retry_number)
42
+ will_retry ? retry_request(retry_number) : response
43
+ end
44
+
45
+ def execute_net_http_request
46
+ uri = request.uri
47
+
48
+ http_response =
49
+ Net::HTTP.start(uri.hostname, uri.port, use_ssl: true, **http_opts) do |http|
50
+ http.max_retries = 0 # we have custom retries logic
51
+ http.request(request.http_request)
52
+ end
53
+
54
+ Response.new(http_response, request: request)
55
+ end
56
+
57
+ def retry_request(current_retry_number)
58
+ sleep_time = retry_sleep_seconds(current_retry_number)
59
+ sleep(sleep_time) if sleep_time.positive?
60
+ execute_request(retry_number: current_retry_number + 1)
61
+ end
62
+
63
+ def retries_limit_reached?(retry_number)
64
+ retry_number >= retries[:count]
65
+ end
66
+
67
+ def retry_sleep_seconds(current_retry_number)
68
+ seconds_per_retry = retries[:sleep]
69
+ seconds_per_retry[current_retry_number] || seconds_per_retry.last || 1
70
+ end
71
+
72
+ def retryable?(response, retry_number)
73
+ response.failed? && response.retryable? && !retries_limit_reached?(retry_number)
74
+ end
75
+
76
+ def unknown_network_error?(error)
77
+ NetworkErrorBuilder::ERRORS.none? { |network_error_class| error.is_a?(network_error_class) }
78
+ end
79
+
80
+ def handle_unknown_error(error)
81
+ raise error
82
+ end
83
+ end
84
+ end
@@ -0,0 +1,23 @@
1
+ # frozen_string_literal: true
2
+
3
+ module MailchimpAPI
4
+ class Resource
5
+ # @return current client
6
+ attr_reader :client
7
+
8
+ # Initializes Resource
9
+ #
10
+ # @param client [Client] current client
11
+ #
12
+ # @return [Collection] APIs collection
13
+ #
14
+ def initialize(client)
15
+ @client = client
16
+ end
17
+
18
+ # @return global client
19
+ def self.client
20
+ MailchimpAPI.client
21
+ end
22
+ end
23
+ end
@@ -0,0 +1,24 @@
1
+ # frozen_string_literal: true
2
+
3
+ module MailchimpAPI
4
+ module Audience
5
+ class MemberTags < Resource
6
+ module APIs
7
+ def list(audience_id, email, query: nil, body: nil, headers: nil)
8
+ path = "/lists/#{audience_id}/members/#{subscriber_hash(email)}/tags"
9
+ client.get(path, query: query, body: body, headers: headers)
10
+ end
11
+
12
+ def create(audience_id, email, query: nil, body: nil, headers: nil)
13
+ path = "/lists/#{audience_id}/members/#{subscriber_hash(email)}/tags"
14
+ client.post(path, query: query, body: body, headers: headers)
15
+ end
16
+ end
17
+
18
+ include MailchimpAPI::Audience::Utils
19
+ extend MailchimpAPI::Audience::Utils
20
+ include APIs
21
+ extend APIs
22
+ end
23
+ end
24
+ end
@@ -0,0 +1,49 @@
1
+ # frozen_string_literal: true
2
+
3
+ module MailchimpAPI
4
+ module Audience
5
+ class Members < Resource
6
+ module APIs
7
+ def list(audience_id, query: nil, body: nil, headers: nil)
8
+ path = "/lists/#{audience_id}/members"
9
+ client.get(path, query: query, body: body, headers: headers)
10
+ end
11
+
12
+ def create(audience_id, query: nil, body: nil, headers: nil)
13
+ path = "/lists/#{audience_id}/members"
14
+ client.post(path, query: query, body: body, headers: headers)
15
+ end
16
+
17
+ def show(audience_id, email, query: nil, body: nil, headers: nil)
18
+ path = "/lists/#{audience_id}/members/#{subscriber_hash(email)}"
19
+ client.get(path, query: query, body: body, headers: headers)
20
+ end
21
+
22
+ def archive(audience_id, email, query: nil, body: nil, headers: nil)
23
+ path = "/lists/#{audience_id}/members/#{subscriber_hash(email)}"
24
+ client.delete(path, query: query, body: body, headers: headers)
25
+ end
26
+
27
+ def update(audience_id, email, query: nil, body: nil, headers: nil)
28
+ path = "/lists/#{audience_id}/members/#{subscriber_hash(email)}"
29
+ client.patch(path, query: query, body: body, headers: headers)
30
+ end
31
+
32
+ def add_or_update(audience_id, email, query: nil, body: nil, headers: nil)
33
+ path = "/lists/#{audience_id}/members/#{subscriber_hash(email)}"
34
+ client.put(path, query: query, body: body, headers: headers)
35
+ end
36
+
37
+ def delete_permanent(audience_id, email, query: nil, body: nil, headers: nil)
38
+ path = "/lists/#{audience_id}/members/#{subscriber_hash(email)}/actions/delete-permanent"
39
+ client.post(path, query: query, body: body, headers: headers)
40
+ end
41
+ end
42
+
43
+ include MailchimpAPI::Audience::Utils
44
+ extend MailchimpAPI::Audience::Utils
45
+ include APIs
46
+ extend APIs
47
+ end
48
+ end
49
+ end
@@ -0,0 +1,21 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "digest"
4
+
5
+ module MailchimpAPI
6
+ module Audience
7
+ module Utils
8
+ class SubscriberHash
9
+ def self.call(email)
10
+ Digest::MD5.hexdigest(email.downcase)
11
+ end
12
+ end
13
+
14
+ private
15
+
16
+ def subscriber_hash(email)
17
+ SubscriberHash.call(email)
18
+ end
19
+ end
20
+ end
21
+ end
@@ -0,0 +1,115 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "json"
4
+
5
+ module MailchimpAPI
6
+ #
7
+ # MailchimpAPI::Response object
8
+ #
9
+ class Response
10
+ # List of Net::HTTP responses that can be retried
11
+ RETRYABLE_RESPONSES = [
12
+ Net::HTTPServerError, # 5xx
13
+ Net::HTTPTooManyRequests # 429
14
+ ].freeze
15
+
16
+ # @return [Net::HTTP::Response] Original Net::HTTP::Response object
17
+ attr_reader :http_response
18
+
19
+ # @return [Request] Request object
20
+ attr_reader :request
21
+
22
+ #
23
+ # Initializes Response object
24
+ #
25
+ # @param http_response [Net::HTTP::Response] original response
26
+ # @param request [Request] Request that generates this response
27
+ #
28
+ # @return [Response] Initialized Response object
29
+ #
30
+ def initialize(http_response, request:)
31
+ @request = request
32
+ @http_response = http_response
33
+ @http_status = nil
34
+ @http_headers = nil
35
+ @http_body = nil
36
+ @body = nil
37
+ end
38
+
39
+ # Parses JSON body if response body contains JSON or returns original
40
+ # http body string
41
+ #
42
+ # @return [Hash, String] Parsed response body (with symbolized keys)
43
+ def body
44
+ @body ||= json_response? ? parse_json(http_body) : http_body
45
+ end
46
+
47
+ # @return [Integer] HTTP status as Integer
48
+ def http_status
49
+ @http_status ||= http_response.code.to_i
50
+ end
51
+
52
+ # @return [Hash] HTTP headers as Hash
53
+ def http_headers
54
+ @http_headers ||= http_response.each_header.to_h
55
+ end
56
+
57
+ # @return [String] Original http body
58
+ def http_body
59
+ @http_body ||= http_response.body
60
+ end
61
+
62
+ # Takes specific key from body, returns nil if key not present in parsed body
63
+ def [](key)
64
+ body[key.to_sym] if body.is_a?(Hash)
65
+ end
66
+
67
+ # Fetches specific key from body, raises error if key not exists
68
+ def fetch(key)
69
+ data = body.is_a?(Hash) ? body : {}
70
+ data.fetch(key.to_sym)
71
+ end
72
+
73
+ # Checks http status code is 2xx
74
+ #
75
+ # @return [Boolean] Returns true if response has success status code (2xx)
76
+ def success?
77
+ http_response.is_a?(Net::HTTPSuccess)
78
+ end
79
+
80
+ # Checks http status code is not 2xx
81
+ #
82
+ # @return [Boolean] Returns true if response has not success status code
83
+ def failed?
84
+ !success?
85
+ end
86
+
87
+ # Checks if response status code is retriable (5xx, 409, 429)
88
+ # @api private
89
+ #
90
+ # @return [Boolean] Returns true if status code is retriable (5xx, 409, 429)
91
+ def retryable?
92
+ failed? && RETRYABLE_RESPONSES.any? { |retryable_class| http_response.is_a?(retryable_class) }
93
+ end
94
+
95
+ #
96
+ # Instance representation string. Default was overwritten to hide secrets
97
+ #
98
+ def inspect
99
+ "#<#{self.class.name} (#{http_response.code})>"
100
+ end
101
+
102
+ private
103
+
104
+ def json_response?
105
+ content_type = http_response["content-type"]
106
+ !content_type.nil? && content_type.include?("json")
107
+ end
108
+
109
+ def parse_json(json)
110
+ JSON.parse(json, symbolize_names: true)
111
+ rescue JSON::ParserError, TypeError
112
+ json
113
+ end
114
+ end
115
+ end
@@ -0,0 +1,37 @@
1
+ # frozen_string_literal: true
2
+
3
+ module MailchimpAPI
4
+ class URIBuilder
5
+ class << self
6
+ def call(url:, path:, query:)
7
+ uri =
8
+ if path.start_with?("/")
9
+ call(url: url, path: path[1, path.size - 1], query: nil)
10
+ elsif path.start_with?("0", "1", "2", "3", "4", "5", "6", "7", "8", "9")
11
+ URI.join(url, "/" + path)
12
+ else
13
+ URI.join(url, path)
14
+ end
15
+
16
+ add_query_params(uri, query)
17
+ uri
18
+ end
19
+
20
+ private
21
+
22
+ def add_query_params(uri, query)
23
+ return if !query || query.empty?
24
+
25
+ # We should merge query params with uri query params to not remove them
26
+ uri_query_string = uri.query
27
+
28
+ if uri_query_string && !uri_query_string.empty?
29
+ old_query = URI.decode_www_form(uri_query_string).to_h
30
+ query = old_query.transform_keys!(&:to_sym).merge!(query.transform_keys(&:to_sym))
31
+ end
32
+
33
+ uri.query = URI.encode_www_form(query)
34
+ end
35
+ end
36
+ end
37
+ end
@@ -0,0 +1,9 @@
1
+ # frozen_string_literal: true
2
+
3
+ module MailchimpAPI
4
+ # MailchimpAPI gem version
5
+ #
6
+ # @return [String] SemVer gem version
7
+ #
8
+ VERSION = File.read(File.join(File.dirname(__FILE__), "../../VERSION")).strip
9
+ end
@@ -0,0 +1,65 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "forwardable"
4
+
5
+ module MailchimpAPI
6
+ API_VERSION = "3.0"
7
+
8
+ class << self
9
+ extend Forwardable
10
+
11
+ attr_writer :client
12
+
13
+ # HTTP methods
14
+ def_delegators :@client,
15
+ :post,
16
+ :get,
17
+ :patch,
18
+ :put,
19
+ :delete
20
+
21
+ # Batch methods
22
+ def_delegators :@client,
23
+ :batch,
24
+ :batch_get_request,
25
+ :batch_post_request,
26
+ :batch_put_request,
27
+ :batch_patch_request,
28
+ :batch_delete_request
29
+
30
+ # Pagination methods
31
+ def_delegators :@client,
32
+ :each_page,
33
+ :each_page_item,
34
+ :next_page
35
+
36
+ # Resources
37
+ def_delegators :@client,
38
+ :audience_members,
39
+ :audience_member_tags
40
+
41
+ def client
42
+ raise "#{name}.client must be set" unless @client
43
+
44
+ @client
45
+ end
46
+ end
47
+ end
48
+
49
+ require_relative "mailchimp-api/resource"
50
+ require_relative "mailchimp-api/resources/audience/utils/subscriber_hash"
51
+ require_relative "mailchimp-api/resources/audience/member_tags"
52
+ require_relative "mailchimp-api/resources/audience/members"
53
+ require_relative "mailchimp-api/batch_request"
54
+ require_relative "mailchimp-api/client/api_methods"
55
+ require_relative "mailchimp-api/client/batch_methods"
56
+ require_relative "mailchimp-api/client/pagination_methods"
57
+ require_relative "mailchimp-api/client"
58
+ require_relative "mailchimp-api/config"
59
+ require_relative "mailchimp-api/error"
60
+ require_relative "mailchimp-api/failed_request_error_builder"
61
+ require_relative "mailchimp-api/network_error_builder"
62
+ require_relative "mailchimp-api/request"
63
+ require_relative "mailchimp-api/request_executor"
64
+ require_relative "mailchimp-api/response"
65
+ require_relative "mailchimp-api/uri_builder"
@@ -0,0 +1,3 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "mailchimp-api"
metadata ADDED
@@ -0,0 +1,65 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: mailchimp-rest-api
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.0.1
5
+ platform: ruby
6
+ authors:
7
+ - Andrey Glushkov
8
+ bindir: bin
9
+ cert_chain: []
10
+ date: 2025-04-28 00:00:00.000000000 Z
11
+ dependencies: []
12
+ description: Marketing Mailchimp REST API with no dependencies.
13
+ email:
14
+ - aglushkov@shakuro.com
15
+ executables: []
16
+ extensions: []
17
+ extra_rdoc_files: []
18
+ files:
19
+ - README.md
20
+ - VERSION
21
+ - lib/mailchimp-api.rb
22
+ - lib/mailchimp-api/batch_request.rb
23
+ - lib/mailchimp-api/client.rb
24
+ - lib/mailchimp-api/client/api_methods.rb
25
+ - lib/mailchimp-api/client/batch_methods.rb
26
+ - lib/mailchimp-api/client/pagination_methods.rb
27
+ - lib/mailchimp-api/config.rb
28
+ - lib/mailchimp-api/error.rb
29
+ - lib/mailchimp-api/failed_request_error_builder.rb
30
+ - lib/mailchimp-api/network_error_builder.rb
31
+ - lib/mailchimp-api/request.rb
32
+ - lib/mailchimp-api/request_executor.rb
33
+ - lib/mailchimp-api/resource.rb
34
+ - lib/mailchimp-api/resources/audience/member_tags.rb
35
+ - lib/mailchimp-api/resources/audience/members.rb
36
+ - lib/mailchimp-api/resources/audience/utils/subscriber_hash.rb
37
+ - lib/mailchimp-api/response.rb
38
+ - lib/mailchimp-api/uri_builder.rb
39
+ - lib/mailchimp-api/version.rb
40
+ - lib/mailchimp-rest-api.rb
41
+ homepage: https://github.com/aglushkov/mailchimp-rest-api
42
+ licenses:
43
+ - MIT
44
+ metadata:
45
+ source_code_uri: https://github.com/aglushkov/mailchimp-rest-api
46
+ documentation_uri: https://www.rubydoc.info/gems/mailchimp-rest-api
47
+ changelog_uri: https://github.com/aglushkov/mailchimp-rest-api/blob/master/CHANGELOG.md
48
+ rdoc_options: []
49
+ require_paths:
50
+ - lib
51
+ required_ruby_version: !ruby/object:Gem::Requirement
52
+ requirements:
53
+ - - ">="
54
+ - !ruby/object:Gem::Version
55
+ version: 2.6.0
56
+ required_rubygems_version: !ruby/object:Gem::Requirement
57
+ requirements:
58
+ - - ">="
59
+ - !ruby/object:Gem::Version
60
+ version: '0'
61
+ requirements: []
62
+ rubygems_version: 3.6.2
63
+ specification_version: 4
64
+ summary: Mailchimp REST API (Marketing)
65
+ test_files: []