mailerlite-ruby 2.0.0 → 2.1.0

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 (71) hide show
  1. checksums.yaml +4 -4
  2. data/.envrc +2 -0
  3. data/.github/workflows/main.yml +1 -1
  4. data/.github/workflows/publish_gem.yml +1 -1
  5. data/.gitignore +2 -1
  6. data/CHANGELOG.md +17 -0
  7. data/README.md +574 -0
  8. data/flake.lock +27 -0
  9. data/flake.nix +25 -0
  10. data/lefthook.yml +14 -0
  11. data/lib/mailerlite/api_response.rb +24 -0
  12. data/lib/mailerlite/automations/automation_emails.rb +57 -0
  13. data/lib/mailerlite/automations/automation_reports.rb +114 -0
  14. data/lib/mailerlite/automations/automation_steps.rb +105 -0
  15. data/lib/mailerlite/automations/automation_templates.rb +34 -0
  16. data/lib/mailerlite/automations/automation_triggers.rb +62 -0
  17. data/lib/mailerlite/automations/automations.rb +177 -8
  18. data/lib/mailerlite/brand_styles/brand_styles.rb +24 -0
  19. data/lib/mailerlite/campaigns/campaigns.rb +33 -1
  20. data/lib/mailerlite/ecommerce/ecommerce.rb +316 -0
  21. data/lib/mailerlite/emails/emails.rb +306 -0
  22. data/lib/mailerlite/errors.rb +88 -0
  23. data/lib/mailerlite/files/files.rb +45 -0
  24. data/lib/mailerlite/groups/groups.rb +16 -8
  25. data/lib/mailerlite/params.rb +58 -0
  26. data/lib/mailerlite/query_params.rb +33 -0
  27. data/lib/mailerlite/request.rb +24 -0
  28. data/lib/mailerlite/segments/segments.rb +10 -10
  29. data/lib/mailerlite/subscribers/subscribers.rb +63 -8
  30. data/lib/mailerlite/version.rb +1 -1
  31. data/lib/mailerlite.rb +14 -0
  32. data/spec/api_response_rspec.rb +140 -0
  33. data/spec/automation_emails_rspec.rb +40 -0
  34. data/spec/automation_reports_rspec.rb +92 -0
  35. data/spec/automation_steps_rspec.rb +76 -0
  36. data/spec/automation_templates_rspec.rb +24 -0
  37. data/spec/automation_triggers_rspec.rb +48 -0
  38. data/spec/automations_rspec.rb +143 -0
  39. data/spec/brand_styles_rspec.rb +49 -0
  40. data/spec/campaigns_rspec.rb +42 -0
  41. data/spec/ecommerce_rspec.rb +141 -0
  42. data/spec/emails_rspec.rb +692 -0
  43. data/spec/files_rspec.rb +90 -0
  44. data/spec/fixtures/brand_styles.json +50 -0
  45. data/spec/fixtures/campaign_simple_editor.json +24 -0
  46. data/spec/fixtures/email.json +81 -0
  47. data/spec/fixtures/email_block.json +12 -0
  48. data/spec/fixtures/email_block_created.json +12 -0
  49. data/spec/fixtures/email_block_created_rejected.json +15 -0
  50. data/spec/fixtures/email_block_moved.json +12 -0
  51. data/spec/fixtures/email_block_updated.json +9 -0
  52. data/spec/fixtures/email_block_updated_rejected.json +13 -0
  53. data/spec/fixtures/email_block_validation_invalid.json +5 -0
  54. data/spec/fixtures/email_etag.json +1 -0
  55. data/spec/fixtures/email_html.json +1 -0
  56. data/spec/fixtures/email_settings.json +111 -0
  57. data/spec/fixtures/email_settings_validation.json +9 -0
  58. data/spec/fixtures/email_template.json +1 -0
  59. data/spec/fixtures/email_version.json +1 -0
  60. data/spec/fixtures/email_versions.json +26 -0
  61. data/spec/fixtures/files.json +44 -0
  62. data/spec/groups_rspec.rb +21 -0
  63. data/spec/params_rspec.rb +126 -0
  64. data/spec/request_rspec.rb +139 -0
  65. data/spec/segments_rspec.rb +39 -0
  66. data/spec/spec_helper.rb +1 -0
  67. data/spec/subscribers_rspec.rb +57 -0
  68. data/spec/support/json_fixture.rb +27 -0
  69. data/spec/support/request_stub.rb +25 -0
  70. metadata +54 -7
  71. /data/{renovate.json → .github/renovate.json} +0 -0
@@ -0,0 +1,306 @@
1
+ # frozen_string_literal: true
2
+
3
+ module MailerLite
4
+ # This is a class for the emails built with the simple editor.
5
+ #
6
+ # To create such an email, create a campaign with `editor: MailerLite::Campaigns::EDITOR_SIMPLE` in the email.
7
+ # The ID of the email is in `data.emails[0].id` of the response.
8
+ #
9
+ # IDs are Strings of the digits 0-9. You can also give them as Integers. An empty ID raises
10
+ # `MailerLite::EmptyIdError`, and an ID with other characters raises `MailerLite::InvalidIdError`.
11
+ #
12
+ # Each method returns the `HTTP::Response` with the `MailerLite::ApiResponse` methods. The methods do not raise
13
+ # for a 4xx or 5xx status: use `response.api_error`. The SDK checks only the IDs in the URL path, and the settings
14
+ # Hash of `update_settings`. It sends all other values as you give them, and the API checks them.
15
+ #
16
+ # Each write needs the etag from the last response for the email, and returns a new etag. The etag covers the
17
+ # whole email, so send the writes to one email one after another. If the email changed after you read it, the
18
+ # API returns 409 and `response.api_error.code` is `VERSION_CONFLICT`. Then `response.api_error.etag` is the
19
+ # current etag.
20
+ #
21
+ # Idempotency keys: if a write fails with a network error, you do not know if the API did the write. Give an
22
+ # `idempotency_key` to the write, and send it again with the same key and the same arguments. If the first request
23
+ # was a success, the API returns the stored response and does not do the write two times. Then
24
+ # `response.idempotent_replay?` is true. Use a new key for each write. The API keeps a key for 24 hours.
25
+ # The key must be 1 to 255 printable ASCII characters. For a key that is not valid, the API returns 400.
26
+ class Emails
27
+ # The error code of a write with an etag that is not the current etag of the email.
28
+ VERSION_CONFLICT = 'version_conflict'
29
+
30
+ # Block types. The API can also return types that are not in this list.
31
+ module BlockType
32
+ BLOCKQUOTE = 'blockquote'
33
+ BULLETED_LIST = 'bulleted_list'
34
+ BUTTON = 'button'
35
+ CODE = 'code'
36
+ DIVIDER = 'divider'
37
+ HEADING = 'heading'
38
+ IMAGE = 'image'
39
+ NUMBERED_LIST = 'numbered_list'
40
+ PARAGRAPH = 'paragraph'
41
+ SOCIAL_ICON = 'social_icon'
42
+ # Only reads return this type. You cannot insert a block of this type.
43
+ TITLE = 'title'
44
+ VIDEO = 'video'
45
+ end
46
+
47
+ # Positions of a block, for `placement`. `BEFORE` and `AFTER` also need a `block_id`.
48
+ module BlockPosition
49
+ AT_START = 'start'
50
+ AT_END = 'end'
51
+ BEFORE = 'before'
52
+ AFTER = 'after'
53
+ end
54
+
55
+ # Where the palette of an email comes from, in `settings[:brand][:mode]`.
56
+ module BrandMode
57
+ ACCOUNT = 'account'
58
+ TEMPLATE = 'template'
59
+ CUSTOM = 'custom'
60
+ end
61
+
62
+ attr_reader :client
63
+
64
+ # Inits the `Emails` class with the specified `client`.
65
+ #
66
+ # @param client [MailerLite::Client] the `Client` instance to use
67
+ def initialize(client: MailerLite::Client.new)
68
+ @client = client
69
+ end
70
+
71
+ # Returns the email with its settings and blocks, and the etag of the email.
72
+ #
73
+ # @param email_id [String, Integer] the ID of the email
74
+ # @return [HTTP::Response] the response from the API
75
+ # @raise [MailerLite::EmptyIdError, MailerLite::InvalidIdError, TypeError] if the ID is not valid
76
+ def get(email_id)
77
+ email_id = Params.path_id(email_id, 'email_id')
78
+ Request.call(client, :get, "/emails/#{email_id}")
79
+ end
80
+
81
+ # Returns the HTML that the email sends, and the etag of the email.
82
+ # The API can return a temporary 503 error. Then try again later.
83
+ #
84
+ # @param email_id [String, Integer] the ID of the email
85
+ # @return [HTTP::Response] the response from the API
86
+ # @raise [MailerLite::EmptyIdError, MailerLite::InvalidIdError, TypeError] if the ID is not valid
87
+ def get_html(email_id)
88
+ email_id = Params.path_id(email_id, 'email_id')
89
+ Request.call(client, :get, "/emails/#{email_id}/html")
90
+ end
91
+
92
+ # Returns the block and its place in the email, and the etag of the email.
93
+ # `placement` has the IDs of the blocks `after` and `before` it, or nil.
94
+ #
95
+ # @param email_id [String, Integer] the ID of the email
96
+ # @param block_id [String, Integer] the ID of the block
97
+ # @return [HTTP::Response] the response from the API
98
+ # @raise [MailerLite::EmptyIdError, MailerLite::InvalidIdError, TypeError] if an ID is not valid
99
+ def get_block(email_id:, block_id:)
100
+ email_id = Params.path_id(email_id, 'email_id')
101
+ block_id = Params.path_id(block_id, 'block_id')
102
+ Request.call(client, :get, "/emails/#{email_id}/blocks/#{block_id}")
103
+ end
104
+
105
+ # Inserts a block in the email. Returns the new block and the new etag of the email.
106
+ #
107
+ # The API drops the attributes and the `mobile` keys that the block type does not declare, and the write still
108
+ # succeeds. Then the response has `rejected` with a `message` and the `keys` that the API dropped, for example
109
+ # `editor_content.attrs.onclick`. When the API drops nothing, the response has no `rejected`.
110
+ #
111
+ # @param email_id [String, Integer] the ID of the email
112
+ # @param etag [String] the etag from the last response for the email
113
+ # @param type [String] the block type, see `BlockType`. You cannot insert `title`.
114
+ # @param editor_content [Hash] the content of the block. It can be empty. Its keys are camelCase, for example `textAlign`.
115
+ # @param placement [Hash, nil] `position` (see `BlockPosition`) and, for `before` and `after`, `block_id`.
116
+ # Without it, the API puts the block at the end of the email.
117
+ # @param idempotency_key [String, nil] a key to send the write again safely, see "Idempotency keys" in the class
118
+ # documentation
119
+ # @return [HTTP::Response] the response from the API
120
+ # @raise [MailerLite::EmptyIdError, MailerLite::InvalidIdError, TypeError] if an ID is not valid
121
+ def create_block(email_id:, etag:, type:, editor_content:, placement: nil, idempotency_key: nil)
122
+ email_id = Params.path_id(email_id, 'email_id')
123
+ body = block_body(type, editor_content, placement).merge('etag' => etag)
124
+ Request.call(client, :post, "/emails/#{email_id}/blocks", json: body, idempotency_key: idempotency_key)
125
+ end
126
+
127
+ # Updates a block of the email. The new content replaces the whole content of the block: the API removes the
128
+ # keys that you do not send. Returns the block and the new etag of the email.
129
+ #
130
+ # The API drops the attributes and the `mobile` keys that the block type does not declare, and the write still
131
+ # succeeds. Then the response has `rejected` with a `message` and the `keys` that the API dropped, for example
132
+ # `editor_content.mobile.onclick`. When the API drops nothing, the response has no `rejected`.
133
+ #
134
+ # @param email_id [String, Integer] the ID of the email
135
+ # @param block_id [String, Integer] the ID of the block
136
+ # @param etag [String] the etag from the last response for the email
137
+ # @param editor_content [Hash] the new content of the block. Its keys are camelCase, for example `textAlign`.
138
+ # @param idempotency_key [String, nil] a key to send the write again safely, see "Idempotency keys" in the class
139
+ # documentation
140
+ # @return [HTTP::Response] the response from the API
141
+ # @raise [MailerLite::EmptyIdError, MailerLite::InvalidIdError, TypeError] if an ID is not valid
142
+ def update_block(email_id:, block_id:, etag:, editor_content:, idempotency_key: nil)
143
+ email_id = Params.path_id(email_id, 'email_id')
144
+ block_id = Params.path_id(block_id, 'block_id')
145
+ body = { 'editor_content' => editor_content, 'etag' => etag }
146
+ Request.call(client, :patch, "/emails/#{email_id}/blocks/#{block_id}", json: body, idempotency_key: idempotency_key)
147
+ end
148
+
149
+ # Deletes a block of the email, and the blocks in it. The response has only the new etag of the email,
150
+ # so the method returns the response and not true.
151
+ #
152
+ # @param email_id [String, Integer] the ID of the email
153
+ # @param block_id [String, Integer] the ID of the block
154
+ # @param etag [String] the etag from the last response for the email
155
+ # @param idempotency_key [String, nil] a key to send the write again safely, see "Idempotency keys" in the class
156
+ # documentation
157
+ # @return [HTTP::Response] the response from the API
158
+ # @raise [MailerLite::EmptyIdError, MailerLite::InvalidIdError, TypeError] if an ID is not valid
159
+ def delete_block(email_id:, block_id:, etag:, idempotency_key: nil)
160
+ email_id = Params.path_id(email_id, 'email_id')
161
+ block_id = Params.path_id(block_id, 'block_id')
162
+ Request.call(client, :delete, "/emails/#{email_id}/blocks/#{block_id}", json: { 'etag' => etag }, idempotency_key: idempotency_key)
163
+ end
164
+
165
+ # Moves a block of the email. Returns the block, its new place in the email, and the new etag of the email.
166
+ #
167
+ # @param email_id [String, Integer] the ID of the email
168
+ # @param block_id [String, Integer] the ID of the block to move
169
+ # @param etag [String] the etag from the last response for the email
170
+ # @param placement [Hash] `position` (see `BlockPosition`) and, for `before` and `after`, `block_id`
171
+ # @param idempotency_key [String, nil] a key to send the write again safely, see "Idempotency keys" in the class
172
+ # documentation
173
+ # @return [HTTP::Response] the response from the API
174
+ # @raise [MailerLite::EmptyIdError, MailerLite::InvalidIdError, TypeError] if an ID is not valid
175
+ def move_block(email_id:, block_id:, etag:, placement:, idempotency_key: nil)
176
+ email_id = Params.path_id(email_id, 'email_id')
177
+ block_id = Params.path_id(block_id, 'block_id')
178
+ body = { 'placement' => placement, 'etag' => etag }
179
+ Request.call(client, :post, "/emails/#{email_id}/blocks/#{block_id}/move", json: body, idempotency_key: idempotency_key)
180
+ end
181
+
182
+ # Validates a block before you insert it. It needs no email and no etag. An invalid block is not an error:
183
+ # the response has `valid` set to false and the `errors` for each field.
184
+ #
185
+ # If a valid block has attributes or `mobile` keys that the block type does not declare, the response has
186
+ # `rejected` with a `message` and these `keys`. `create_block` and `update_block` drop these keys.
187
+ # When `valid` is false, the response has no `rejected`.
188
+ #
189
+ # @param type [String] the block type, see `BlockType`
190
+ # @param editor_content [Hash] the content of the block. Its keys are camelCase, for example `textAlign`.
191
+ # @param placement [Hash, nil] `position` (see `BlockPosition`) and, for `before` and `after`, `block_id`
192
+ # @return [HTTP::Response] the response from the API, with `valid`, `errors`, `schema` and, in some cases, `rejected`
193
+ def validate_block(type:, editor_content:, placement: nil)
194
+ Request.call(client, :post, '/emails/blocks/validate', json: block_body(type, editor_content, placement))
195
+ end
196
+
197
+ # Updates the design settings of the email. Returns the settings and the new etag of the email.
198
+ #
199
+ # Send only the keys to change, in the shape of `settings` in the response of `get`. The groups are `brand`,
200
+ # `layout`, `typography`, `button` and `footer`. The SDK sends exactly the keys of the Hash:
201
+ # - a key that is not in the Hash does not change;
202
+ # - an empty String unsets a brand colour, and the email then uses the account value;
203
+ # - an empty editor document, `{ editor_content: {} }` or `{ editor_content: { content: [] } }`, unsets a footer text;
204
+ # - other colours, sizes, `button.border_radius` and `brand.style` cannot be empty: an empty String gives a `422` error;
205
+ # - any other value is the new value. Send style values as Strings, for example '16'.
206
+ #
207
+ # The API stores each value that you send, also a value that the email inherits from the account.
208
+ # `footer.social_links` replaces the whole list: keep the `link_id` of each link to keep its click history.
209
+ # To apply a preset palette, send its `identifier` from `BrandStyles#get` in `brand.style`.
210
+ # The response can have `rejected` when the API removed unsafe values.
211
+ #
212
+ # @param email_id [String, Integer] the ID of the email
213
+ # @param etag [String] the etag from the last response for the email
214
+ # @param settings [Hash] the settings to change, without `etag`
215
+ # @param idempotency_key [String, nil] a key to send the write again safely, see "Idempotency keys" in the class
216
+ # documentation
217
+ # @return [HTTP::Response] the response from the API
218
+ # @raise [MailerLite::EmptyIdError, MailerLite::InvalidIdError, TypeError] if the ID is not valid
219
+ # @raise [ArgumentError] if the settings contain an `etag` key
220
+ # @raise [TypeError] if the settings are not a Hash
221
+ def update_settings(email_id:, etag:, settings:, idempotency_key: nil)
222
+ email_id = Params.path_id(email_id, 'email_id')
223
+ body = Params.settings(settings).merge('etag' => etag)
224
+ Request.call(client, :patch, "/emails/#{email_id}/settings", json: body, idempotency_key: idempotency_key)
225
+ end
226
+
227
+ # Validates a change to the design settings of the email with the rules of `update_settings`, and saves nothing.
228
+ # It needs no etag. Invalid settings are not an error: the response has `valid` set to false and the `errors`
229
+ # for each field. The response can also have `rejected`.
230
+ #
231
+ # @param email_id [String, Integer] the ID of the email
232
+ # @param settings [Hash] the settings that you want to send to `update_settings`, without `etag`
233
+ # @return [HTTP::Response] the response from the API, with `valid`, `errors` and `schema`
234
+ # @raise [MailerLite::EmptyIdError, MailerLite::InvalidIdError, TypeError] if the ID is not valid
235
+ def validate_settings(email_id:, settings:)
236
+ email_id = Params.path_id(email_id, 'email_id')
237
+ Request.call(client, :post, "/emails/#{email_id}/settings/validate", json: settings)
238
+ end
239
+
240
+ # Saves the email as a new template with the name. The API cannot delete templates.
241
+ #
242
+ # @param email_id [String, Integer] the ID of the email
243
+ # @param name [String] the name of the template
244
+ # @param idempotency_key [String, nil] a key to send the write again safely, see "Idempotency keys" in the class
245
+ # documentation
246
+ # @return [HTTP::Response] the response from the API
247
+ # @raise [MailerLite::EmptyIdError, MailerLite::InvalidIdError, TypeError] if the ID is not valid
248
+ def create_template(email_id:, name:, idempotency_key: nil)
249
+ email_id = Params.path_id(email_id, 'email_id')
250
+ Request.call(client, :post, "/emails/#{email_id}/templates", json: { 'name' => name }, idempotency_key: idempotency_key)
251
+ end
252
+
253
+ # Returns the saved versions of the email, newest first. `current` marks the version that the email has now.
254
+ #
255
+ # @param email_id [String, Integer] the ID of the email
256
+ # @param page [Integer] the page number of the results to return
257
+ # @param limit [Integer] the maximum number of versions to return, from 1 to 100
258
+ # @return [HTTP::Response] the response from the API
259
+ # @raise [MailerLite::EmptyIdError, MailerLite::InvalidIdError, TypeError] if the ID is not valid
260
+ def list_versions(email_id, page: nil, limit: nil)
261
+ email_id = Params.path_id(email_id, 'email_id')
262
+ Request.call(client, :get, "/emails/#{email_id}/versions", query: Params.query(page: page, limit: limit))
263
+ end
264
+
265
+ # Saves the content and the settings of the email as a new version. If nothing changed after the last version,
266
+ # the API saves no new version and returns the last version. The status code (`response.code`) is 201 for a new
267
+ # version and 200 for the last version. Both are a success.
268
+ #
269
+ # @param email_id [String, Integer] the ID of the email
270
+ # @param idempotency_key [String, nil] a key to send the write again safely, see "Idempotency keys" in the class
271
+ # documentation
272
+ # @return [HTTP::Response] the response from the API
273
+ # @raise [MailerLite::EmptyIdError, MailerLite::InvalidIdError, TypeError] if the ID is not valid
274
+ def create_version(email_id, idempotency_key: nil)
275
+ email_id = Params.path_id(email_id, 'email_id')
276
+ Request.call(client, :post, "/emails/#{email_id}/versions", idempotency_key: idempotency_key)
277
+ end
278
+
279
+ # Restores the email to a version. Returns the email in the shape of `get`, with the new etag. Before the
280
+ # restore, the API saves the current state as a version, so you can undo the restore.
281
+ #
282
+ # @param email_id [String, Integer] the ID of the email
283
+ # @param version_id [String, Integer] the ID of the version
284
+ # @param etag [String] the etag from the last response for the email
285
+ # @param idempotency_key [String, nil] a key to send the write again safely, see "Idempotency keys" in the class
286
+ # documentation
287
+ # @return [HTTP::Response] the response from the API
288
+ # @raise [MailerLite::EmptyIdError, MailerLite::InvalidIdError, TypeError] if an ID is not valid
289
+ def restore_version(email_id:, version_id:, etag:, idempotency_key: nil)
290
+ email_id = Params.path_id(email_id, 'email_id')
291
+ version_id = Params.path_id(version_id, 'version_id')
292
+ Request.call(client, :post, "/emails/#{email_id}/versions/#{version_id}/restore", json: { 'etag' => etag }, idempotency_key: idempotency_key)
293
+ end
294
+
295
+ private
296
+
297
+ def block_body(type, editor_content, placement)
298
+ body = {
299
+ 'type' => type,
300
+ 'editor_content' => editor_content
301
+ }
302
+ body['placement'] = placement unless placement.nil?
303
+ body
304
+ end
305
+ end
306
+ end
@@ -0,0 +1,88 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'json'
4
+
5
+ module MailerLite
6
+ # The base class of the errors of the SDK.
7
+ class Error < StandardError; end
8
+
9
+ # An error response (4xx or 5xx) from the API.
10
+ #
11
+ # The SDK does not raise it. Get it with `response.api_error` and raise it if you want to.
12
+ # It is not an `HTTP::Error`, so code that retries on network errors does not also retry an API error.
13
+ # It keeps only plain values, so `Marshal.dump` works.
14
+ #
15
+ # Note: `HTTP::Response#code` is the numeric status. `ApiError#code` is the String code of the API,
16
+ # for example `"version_conflict"`. The numeric status is in `ApiError#status`.
17
+ class ApiError < Error
18
+ # @return [Integer] the HTTP status code, for example 409
19
+ attr_reader :status
20
+ # @return [String, nil] the error code of the API, for example "version_conflict"
21
+ attr_reader :code
22
+ # @return [String, nil] the current etag of the email, for a 409 version conflict
23
+ attr_reader :etag
24
+ # @return [Hash, nil] the error messages for each field, for some 422 responses
25
+ attr_reader :errors
26
+ # @return [String, nil] the message of the API
27
+ attr_reader :api_message
28
+ # @return [String] the response body as it is
29
+ attr_reader :body
30
+ # @return [Hash] the response headers
31
+ attr_reader :headers
32
+
33
+ # Makes an error from a response. It works for the responses of all methods of the SDK.
34
+ # A body that is not a JSON object gives nil fields.
35
+ #
36
+ # @param response [HTTP::Response] the response
37
+ # @return [MailerLite::ApiError] the error
38
+ def self.from_response(response)
39
+ body = response.body.to_s
40
+ data = parse(body)
41
+ new(status: response.code, reason: response.reason, body: body, headers: response.headers.to_h,
42
+ code: data['code'], etag: data['etag'], errors: data['errors'], api_message: data['message'])
43
+ end
44
+
45
+ def self.parse(body)
46
+ data = JSON.parse(body)
47
+ data.is_a?(Hash) ? data : {}
48
+ rescue JSON::ParserError, EncodingError
49
+ {}
50
+ end
51
+ private_class_method :parse
52
+
53
+ # @param status [Integer] the HTTP status code
54
+ # @param reason [String, nil] the reason phrase of the status
55
+ # @param code [String, nil] the error code of the API
56
+ # @param etag [String, nil] the current etag of the email
57
+ # @param errors [Hash, nil] the error messages for each field
58
+ # @param api_message [String, nil] the message of the API
59
+ # @param body [String] the response body
60
+ # @param headers [Hash] the response headers
61
+ def initialize(status:, reason: nil, code: nil, etag: nil, errors: nil, api_message: nil, body: '', headers: {})
62
+ @status = status
63
+ @code = code
64
+ @etag = etag
65
+ @errors = errors
66
+ @api_message = api_message
67
+ @body = body
68
+ @headers = headers
69
+ super(build_message(reason))
70
+ end
71
+
72
+ private
73
+
74
+ # For example "409 Conflict (version_conflict): The email was changed."
75
+ def build_message(reason)
76
+ text = [status, reason].compact.join(' ')
77
+ text += " (#{code})" if code
78
+ text += ": #{api_message}" if api_message
79
+ text
80
+ end
81
+ end
82
+
83
+ # An ID for the URL path is empty. The SDK sends no request.
84
+ class EmptyIdError < ArgumentError; end
85
+
86
+ # An ID for the URL path does not contain only the digits 0-9. The SDK sends no request.
87
+ class InvalidIdError < ArgumentError; end
88
+ end
@@ -0,0 +1,45 @@
1
+ # frozen_string_literal: true
2
+
3
+ module MailerLite
4
+ # This is a class for the files of the media library from MailerLite API.
5
+ class Files
6
+ # File types for `filter: { type: ... }`. The API can also return types that are not in this list.
7
+ module FileType
8
+ IMAGE = 'image'
9
+ PDF = 'pdf'
10
+ EPUB = 'epub'
11
+ end
12
+
13
+ # Sort values. The values that start with `-` sort in descending order.
14
+ module FileSort
15
+ CREATED_AT = 'created_at'
16
+ CREATED_AT_DESC = '-created_at'
17
+ NAME = 'name'
18
+ NAME_DESC = '-name'
19
+ SIZE = 'size'
20
+ SIZE_DESC = '-size'
21
+ end
22
+
23
+ attr_reader :client
24
+
25
+ # Inits the `Files` class with the specified `client`.
26
+ #
27
+ # @param client [MailerLite::Client] the `Client` instance to use
28
+ def initialize(client: MailerLite::Client.new)
29
+ @client = client
30
+ end
31
+
32
+ # Returns the files of the media library. Use their `url` in image blocks.
33
+ # `width` and `height` are nil for files that are not images. The SDK does not send nil values.
34
+ #
35
+ # @param filter [Hash, nil] `name` (a part of the file name) and `type` (see `FileType`)
36
+ # @param sort [String, nil] see `FileSort`. The default is `-created_at`.
37
+ # @param page [Integer, nil] the page number of the results to return
38
+ # @param limit [Integer, nil] the maximum number of files to return, from 1 to 100
39
+ # @return [HTTP::Response] the response from the API, with the `MailerLite::ApiResponse` methods
40
+ # @raise [TypeError] if the filter is not a Hash
41
+ def list(filter: nil, sort: nil, page: nil, limit: nil)
42
+ Request.call(client, :get, '/files', query: Params.query(filter: filter, sort: sort, page: page, limit: limit))
43
+ end
44
+ end
45
+ end
@@ -52,15 +52,23 @@ module MailerLite
52
52
  # @param group_id [Integer] The id of existing group belonging to the account
53
53
  # @param filter[:status] [String] Must be one of the possible statuses: active, unsubscribed, unconfirmed, bounced or junk. Defaults to active.
54
54
  # @param limit [Integer] the maximum number of subscribers to return
55
- # @param page [Integer] the page number of the results to return
55
+ # @param cursor [String] the cursor for the page of results to return
56
+ # @param include [String] the relations to embed, only groups is supported
56
57
  # @return [HTTP::Response] the response from the API
57
- def get_subscribers(group_id:, filter: {}, limit: nil, cursor: nil, sort: nil)
58
- params = {}
59
- params['filter[status]'] = filter[:status] if filter.key?(:status)
60
- params['limit'] = limit if limit
61
- params['sort'] = sort if sort
62
- params['cursor'] = cursor if cursor
63
- client.http.get("#{MAILERLITE_API_URL}/groups/#{group_id}/subscribers", json: params.compact)
58
+ def get_subscribers(group_id:, filter: {}, limit: nil, cursor: nil, sort: nil, include: nil)
59
+ params = QueryParams.filter(filter, %i[status]).merge('limit' => limit, 'sort' => sort, 'cursor' => cursor, 'include' => include)
60
+ client.http.get(QueryParams.uri("#{MAILERLITE_API_URL}/groups/#{group_id}/subscribers", params))
61
+ end
62
+
63
+ # Imports subscribers into the specified group.
64
+ # @param group_id [Integer] The id of existing group belonging to the account
65
+ # @param subscribers [Array] the subscribers to import, each a hash with email, and optional name and fields
66
+ # @param resubscribe [Boolean] reactivate unsubscribed subscribers, bounced and junk subscribers are not reactivated
67
+ # @param trigger_automations [Boolean] start the automations the import qualifies for
68
+ # @return [HTTP::Response] the response from the API
69
+ def import_subscribers(group_id:, subscribers:, resubscribe: nil, trigger_automations: nil)
70
+ params = { 'subscribers' => subscribers, 'resubscribe' => resubscribe, 'trigger_automations' => trigger_automations }
71
+ client.http.post("#{MAILERLITE_API_URL}/groups/#{group_id}/import-subscribers", json: params.compact)
64
72
  end
65
73
 
66
74
  # Assign Subscriber to the specified group.
@@ -0,0 +1,58 @@
1
+ # frozen_string_literal: true
2
+
3
+ module MailerLite
4
+ # Checks and helpers for the request parameters of the resources.
5
+ # The SDK checks only the IDs in the URL path and the values that its code reads. The API checks all other values.
6
+ # Each check raises before the request, so the SDK sends no request when a value is not valid.
7
+ module Params
8
+ module_function
9
+
10
+ # Use \A and \z: `$` and \Z also match before a final new line.
11
+ # Use [0-9]: [[:digit:]] also matches digits that are not ASCII.
12
+ ID_PATTERN = /\A[0-9]+\z/
13
+
14
+ def path_id(value, name)
15
+ raise EmptyIdError, "`#{name}` must not be empty." if value.nil? || value == ''
16
+
17
+ raise TypeError, "`#{name}` type is not valid. Expected String or Integer, got #{value.class}." unless value.is_a?(String) || value.is_a?(Integer)
18
+
19
+ id = value.to_s
20
+ # match? raises for a String with invalid bytes, so check the encoding first.
21
+ raise InvalidIdError, "`#{name}` must contain only the digits 0-9, got #{id.inspect}." unless id.valid_encoding? && id.ascii_only? && id.match?(ID_PATTERN)
22
+
23
+ id
24
+ end
25
+
26
+ def require_hash(value, name)
27
+ raise TypeError, "`#{name}` type is not valid. Expected Hash, got #{value.class}." unless value.is_a?(Hash)
28
+
29
+ value
30
+ end
31
+
32
+ def settings(settings)
33
+ require_hash(settings, 'settings')
34
+ # The body has the settings and the etag at the same level. An etag key in the settings can hide the etag argument.
35
+ raise ArgumentError, '`settings` must not contain `etag`. Use the `etag` argument.' if settings.key?(:etag) || settings.key?('etag')
36
+
37
+ settings
38
+ end
39
+
40
+ # It drops nil values and keeps empty Strings.
41
+ def query(filter: nil, **params)
42
+ query = {}
43
+ unless filter.nil?
44
+ require_hash(filter, 'filter').each do |key, value|
45
+ query["filter[#{key}]"] = value unless value.nil?
46
+ end
47
+ end
48
+ params.each { |key, value| query[key.to_s] = value unless value.nil? }
49
+ query
50
+ end
51
+
52
+ def idempotency_headers(key)
53
+ return {} if key.nil?
54
+
55
+ { 'Idempotency-Key' => key }
56
+ end
57
+ end
58
+ end
@@ -0,0 +1,33 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'uri'
4
+
5
+ module MailerLite
6
+ # Encodes values for the query string of the MailerLite API.
7
+ module QueryParams
8
+ module_function
9
+
10
+ def boolean(value)
11
+ return if value.nil?
12
+
13
+ value ? 1 : 0
14
+ end
15
+
16
+ def filter(filter, keys)
17
+ filter.each_with_object({}) do |(key, value), params|
18
+ params["filter[#{key}]"] = value.is_a?(Array) ? value.join(',') : value if keys.include?(key)
19
+ end
20
+ end
21
+
22
+ def report(start_date:, end_date:, shop_id:, excel:)
23
+ { 'start_date' => start_date, 'end_date' => end_date, 'shop_id' => shop_id, 'excel' => boolean(excel) }
24
+ end
25
+
26
+ def uri(url, params)
27
+ uri = URI(url)
28
+ query = URI.encode_www_form(params.compact)
29
+ uri.query = query unless query.empty?
30
+ uri
31
+ end
32
+ end
33
+ end
@@ -0,0 +1,24 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'uri'
4
+
5
+ module MailerLite
6
+ # Sends the requests of `Emails`, `BrandStyles` and `Files`.
7
+ module Request
8
+ module_function
9
+
10
+ # It does not raise for a 4xx or 5xx status.
11
+ def call(client, method, path, json: nil, query: nil, idempotency_key: nil)
12
+ headers = Params.idempotency_headers(idempotency_key)
13
+ # Read the constant here, not when the file loads.
14
+ uri = URI("#{MAILERLITE_API_URL}#{path}")
15
+ uri.query = URI.encode_www_form(query) if query && !query.empty?
16
+ options = {}
17
+ # `body: nil.to_json` sends "null", so add `json:` only when there is a body.
18
+ options[:json] = json unless json.nil?
19
+ http = client.http
20
+ http = http.headers(headers) unless headers.empty?
21
+ http.request(method, uri, options).extend(ApiResponse)
22
+ end
23
+ end
24
+ end
@@ -16,22 +16,20 @@ module MailerLite
16
16
  #
17
17
  # @param limit [Integer] the maximum number of Segments to return
18
18
  # @param page [Integer] the page number of the results to return
19
+ # @param sort [String] the field to sort by, prefix with - for descending order
19
20
  # @return [HTTP::Response] the response from the API
20
- def list(limit: nil, page: nil)
21
- params = {}
22
- params['limit'] = limit if limit
23
- params['page'] = page if page
24
-
25
- client.http.get("#{MAILERLITE_API_URL}/segments", json: params.compact)
21
+ def list(limit: nil, page: nil, sort: nil)
22
+ client.http.get(QueryParams.uri("#{MAILERLITE_API_URL}/segments", { 'limit' => limit, 'page' => page, 'sort' => sort }))
26
23
  end
27
24
 
28
25
  # Update the specified Segment
29
26
  #
30
27
  # @param segment_id [String] the ID of the Segments to update
31
28
  # @param name [String] the name to update
29
+ # @param filter [Array] the segment rules
32
30
  # @return [HTTP::Response] the response from the API
33
- def update(segment_id:, name:)
34
- params = { 'name' => name }
31
+ def update(segment_id:, name:, filter: nil)
32
+ params = { 'name' => name, 'filter' => filter }
35
33
  client.http.put("#{MAILERLITE_API_URL}/segments/#{segment_id}", json: params.compact)
36
34
  end
37
35
 
@@ -39,12 +37,14 @@ module MailerLite
39
37
  # @param segment_id [Integer] The id of existing Segment belonging to the account
40
38
  # @param filter[status] [String] Must be one of the possible statuses: active, unsubscribed, unconfirmed, bounced or junk. Defaults to active.
41
39
  # @param limit [Integer] the maximum number of subscribers to return
42
- # @param after [Integer] The last subscriber id, available in meta.last
40
+ # @param cursor [String] the cursor for the page of results to return, available in meta.next_cursor
41
+ # @param after [Integer] deprecated, the API ignores it, use cursor
43
42
  # @return [HTTP::Response] the response from the API
44
- def get_subscribers(segment_id:, filter: {}, limit: nil, after: nil)
43
+ def get_subscribers(segment_id:, filter: {}, limit: nil, after: nil, cursor: nil)
45
44
  params = {}
46
45
  params['filter[status]'] = filter[:status] if filter.key?(:status)
47
46
  params['limit'] = limit if limit
47
+ params['cursor'] = cursor if cursor
48
48
  params['after'] = after if after
49
49
  uri = URI("#{MAILERLITE_API_URL}/segments/#{segment_id}/subscribers")
50
50
  uri.query = URI.encode_www_form(params.compact)