laneful-ruby 1.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 +7 -0
- data/.rubocop.yml +31 -0
- data/LICENSE +21 -0
- data/Makefile +49 -0
- data/README.md +456 -0
- data/Rakefile +8 -0
- data/examples/Gemfile +5 -0
- data/examples/README.md +53 -0
- data/examples/simple_example.rb +121 -0
- data/laneful-ruby.gemspec +38 -0
- data/lib/laneful/client.rb +123 -0
- data/lib/laneful/exceptions.rb +41 -0
- data/lib/laneful/models.rb +442 -0
- data/lib/laneful/version.rb +5 -0
- data/lib/laneful/webhooks.rb +38 -0
- data/lib/laneful.rb +27 -0
- data/scripts/publish.sh +169 -0
- metadata +93 -0
@@ -0,0 +1,442 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Laneful
|
4
|
+
# Represents an email address with an optional name
|
5
|
+
class Address
|
6
|
+
attr_reader :email, :name
|
7
|
+
|
8
|
+
def initialize(email, name = nil)
|
9
|
+
@email = email
|
10
|
+
@name = name
|
11
|
+
validate!
|
12
|
+
end
|
13
|
+
|
14
|
+
# Creates an Address from a hash representation
|
15
|
+
def self.from_hash(data)
|
16
|
+
new(data['email'], data['name'])
|
17
|
+
end
|
18
|
+
|
19
|
+
def to_hash
|
20
|
+
hash = { 'email' => email }
|
21
|
+
hash['name'] = name if name && !name.empty?
|
22
|
+
hash
|
23
|
+
end
|
24
|
+
|
25
|
+
def ==(other)
|
26
|
+
return false unless other.is_a?(Address)
|
27
|
+
|
28
|
+
email == other.email && name == other.name
|
29
|
+
end
|
30
|
+
|
31
|
+
def eql?(other)
|
32
|
+
self == other
|
33
|
+
end
|
34
|
+
|
35
|
+
def hash
|
36
|
+
[email, name].hash
|
37
|
+
end
|
38
|
+
|
39
|
+
def to_s
|
40
|
+
if name && !name.strip.empty?
|
41
|
+
"#{name} <#{email}>"
|
42
|
+
else
|
43
|
+
email
|
44
|
+
end
|
45
|
+
end
|
46
|
+
|
47
|
+
private
|
48
|
+
|
49
|
+
def validate!
|
50
|
+
raise ValidationException, 'Email address cannot be empty' if email.nil? || email.strip.empty?
|
51
|
+
|
52
|
+
# Basic email validation
|
53
|
+
email_regex = /^[A-Za-z0-9+_.-]+@[A-Za-z0-9.-]+\.[A-Za-z]{2,}$/
|
54
|
+
return if email.match?(email_regex)
|
55
|
+
|
56
|
+
raise ValidationException, "Invalid email address format: #{email}"
|
57
|
+
end
|
58
|
+
end
|
59
|
+
|
60
|
+
# Configuration for email tracking settings
|
61
|
+
class TrackingSettings
|
62
|
+
attr_reader :opens, :clicks, :unsubscribes
|
63
|
+
|
64
|
+
def initialize(opens: false, clicks: false, unsubscribes: false)
|
65
|
+
@opens = opens
|
66
|
+
@clicks = clicks
|
67
|
+
@unsubscribes = unsubscribes
|
68
|
+
end
|
69
|
+
|
70
|
+
# Creates tracking settings from a hash representation
|
71
|
+
def self.from_hash(data)
|
72
|
+
new(
|
73
|
+
opens: data['opens'] || false,
|
74
|
+
clicks: data['clicks'] || false,
|
75
|
+
unsubscribes: data['unsubscribes'] || false
|
76
|
+
)
|
77
|
+
end
|
78
|
+
|
79
|
+
def to_hash
|
80
|
+
{
|
81
|
+
'opens' => opens,
|
82
|
+
'clicks' => clicks,
|
83
|
+
'unsubscribes' => unsubscribes
|
84
|
+
}
|
85
|
+
end
|
86
|
+
|
87
|
+
def ==(other)
|
88
|
+
return false unless other.is_a?(TrackingSettings)
|
89
|
+
|
90
|
+
opens == other.opens && clicks == other.clicks && unsubscribes == other.unsubscribes
|
91
|
+
end
|
92
|
+
|
93
|
+
def eql?(other)
|
94
|
+
self == other
|
95
|
+
end
|
96
|
+
|
97
|
+
def hash
|
98
|
+
[opens, clicks, unsubscribes].hash
|
99
|
+
end
|
100
|
+
|
101
|
+
def to_s
|
102
|
+
"TrackingSettings{opens=#{opens}, clicks=#{clicks}, unsubscribes=#{unsubscribes}}"
|
103
|
+
end
|
104
|
+
end
|
105
|
+
|
106
|
+
# Represents a file attachment for an email
|
107
|
+
class Attachment
|
108
|
+
attr_reader :filename, :content_type, :content
|
109
|
+
|
110
|
+
def initialize(filename, content_type, content)
|
111
|
+
@filename = filename
|
112
|
+
@content_type = content_type
|
113
|
+
@content = content
|
114
|
+
validate!
|
115
|
+
end
|
116
|
+
|
117
|
+
# Creates an attachment from a file
|
118
|
+
def self.from_file(file_path)
|
119
|
+
filename = File.basename(file_path)
|
120
|
+
content_type = detect_content_type(file_path)
|
121
|
+
content = Base64.strict_encode64(File.binread(file_path))
|
122
|
+
new(filename, content_type, content)
|
123
|
+
end
|
124
|
+
|
125
|
+
# Creates an attachment from a hash representation
|
126
|
+
def self.from_hash(data)
|
127
|
+
new(data['filename'], data['content_type'], data['content'])
|
128
|
+
end
|
129
|
+
|
130
|
+
def to_hash
|
131
|
+
{
|
132
|
+
'filename' => filename,
|
133
|
+
'content_type' => content_type,
|
134
|
+
'content' => content
|
135
|
+
}
|
136
|
+
end
|
137
|
+
|
138
|
+
def ==(other)
|
139
|
+
return false unless other.is_a?(Attachment)
|
140
|
+
|
141
|
+
filename == other.filename && content_type == other.content_type && content == other.content
|
142
|
+
end
|
143
|
+
|
144
|
+
def eql?(other)
|
145
|
+
self == other
|
146
|
+
end
|
147
|
+
|
148
|
+
def hash
|
149
|
+
[filename, content_type, content].hash
|
150
|
+
end
|
151
|
+
|
152
|
+
def to_s
|
153
|
+
"Attachment{filename='#{filename}', contentType='#{content_type}', contentLength=#{content&.length || 0}}"
|
154
|
+
end
|
155
|
+
|
156
|
+
def self.detect_content_type(file_path)
|
157
|
+
# Simple content type detection based on file extension
|
158
|
+
case File.extname(file_path).downcase
|
159
|
+
when '.pdf'
|
160
|
+
'application/pdf'
|
161
|
+
when '.jpg', '.jpeg'
|
162
|
+
'image/jpeg'
|
163
|
+
when '.png'
|
164
|
+
'image/png'
|
165
|
+
when '.gif'
|
166
|
+
'image/gif'
|
167
|
+
when '.txt'
|
168
|
+
'text/plain'
|
169
|
+
when '.html', '.htm'
|
170
|
+
'text/html'
|
171
|
+
when '.doc'
|
172
|
+
'application/msword'
|
173
|
+
when '.docx'
|
174
|
+
'application/vnd.openxmlformats-officedocument.wordprocessingml.document'
|
175
|
+
when '.xls'
|
176
|
+
'application/vnd.ms-excel'
|
177
|
+
when '.xlsx'
|
178
|
+
'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet'
|
179
|
+
else
|
180
|
+
'application/octet-stream'
|
181
|
+
end
|
182
|
+
end
|
183
|
+
|
184
|
+
private
|
185
|
+
|
186
|
+
def validate!
|
187
|
+
raise ValidationException, 'Filename cannot be empty' if filename.nil? || filename.strip.empty?
|
188
|
+
raise ValidationException, 'Content type cannot be empty' if content_type.nil? || content_type.strip.empty?
|
189
|
+
raise ValidationException, 'Content cannot be empty' if content.nil? || content.strip.empty?
|
190
|
+
end
|
191
|
+
end
|
192
|
+
|
193
|
+
# Represents a single email to be sent
|
194
|
+
class Email
|
195
|
+
attr_reader :from, :to, :cc, :bcc, :subject, :text_content, :html_content,
|
196
|
+
:template_id, :template_data, :attachments, :headers, :reply_to,
|
197
|
+
:send_time, :webhook_data, :tag, :tracking
|
198
|
+
|
199
|
+
def initialize(builder)
|
200
|
+
@from = builder.instance_variable_get(:@from)
|
201
|
+
@to = builder.instance_variable_get(:@to).dup.freeze
|
202
|
+
@cc = builder.instance_variable_get(:@cc).dup.freeze
|
203
|
+
@bcc = builder.instance_variable_get(:@bcc).dup.freeze
|
204
|
+
@subject = builder.instance_variable_get(:@subject)
|
205
|
+
@text_content = builder.instance_variable_get(:@text_content)
|
206
|
+
@html_content = builder.instance_variable_get(:@html_content)
|
207
|
+
@template_id = builder.instance_variable_get(:@template_id)
|
208
|
+
@template_data = builder.instance_variable_get(:@template_data)&.dup&.freeze
|
209
|
+
@attachments = builder.instance_variable_get(:@attachments).dup.freeze
|
210
|
+
@headers = builder.instance_variable_get(:@headers)&.dup&.freeze
|
211
|
+
@reply_to = builder.instance_variable_get(:@reply_to)
|
212
|
+
@send_time = builder.instance_variable_get(:@send_time)
|
213
|
+
@webhook_data = builder.instance_variable_get(:@webhook_data)&.dup&.freeze
|
214
|
+
@tag = builder.instance_variable_get(:@tag)
|
215
|
+
@tracking = builder.instance_variable_get(:@tracking)
|
216
|
+
validate!
|
217
|
+
end
|
218
|
+
|
219
|
+
# Creates an Email from a hash representation
|
220
|
+
def self.from_hash(data)
|
221
|
+
builder = Builder.new
|
222
|
+
|
223
|
+
builder.from(Address.from_hash(data['from'])) if data && data['from']
|
224
|
+
|
225
|
+
data['to'].each { |to_data| builder.to(Address.from_hash(to_data)) } if data && data['to']
|
226
|
+
|
227
|
+
data['cc'].each { |cc_data| builder.cc(Address.from_hash(cc_data)) } if data && data['cc']
|
228
|
+
|
229
|
+
data['bcc'].each { |bcc_data| builder.bcc(Address.from_hash(bcc_data)) } if data && data['bcc']
|
230
|
+
|
231
|
+
builder.subject(data['subject']) if data && data['subject']
|
232
|
+
builder.text_content(data['text_content']) if data && data['text_content']
|
233
|
+
builder.html_content(data['html_content']) if data && data['html_content']
|
234
|
+
builder.template_id(data['template_id']) if data && data['template_id']
|
235
|
+
builder.template_data(data['template_data']) if data && data['template_data']
|
236
|
+
builder.headers(data['headers']) if data && data['headers']
|
237
|
+
builder.reply_to(Address.from_hash(data['reply_to'])) if data && data['reply_to']
|
238
|
+
builder.send_time(data['send_time']) if data && data['send_time']
|
239
|
+
builder.webhook_data(data['webhook_data']) if data && data['webhook_data']
|
240
|
+
builder.tag(data['tag']) if data && data['tag']
|
241
|
+
builder.tracking(TrackingSettings.from_hash(data['tracking'])) if data && data['tracking']
|
242
|
+
|
243
|
+
if data && data['attachments']
|
244
|
+
data['attachments'].each { |attachment_data| builder.attachment(Attachment.from_hash(attachment_data)) }
|
245
|
+
end
|
246
|
+
|
247
|
+
builder.build
|
248
|
+
end
|
249
|
+
|
250
|
+
def to_hash
|
251
|
+
hash = {}
|
252
|
+
|
253
|
+
# Required fields
|
254
|
+
hash['from'] = from.to_hash
|
255
|
+
hash['to'] = to.map(&:to_hash)
|
256
|
+
hash['subject'] = subject if subject
|
257
|
+
|
258
|
+
# Optional fields (only include if not nil/empty)
|
259
|
+
hash['cc'] = cc.map(&:to_hash) unless cc.empty?
|
260
|
+
hash['bcc'] = bcc.map(&:to_hash) unless bcc.empty?
|
261
|
+
hash['text_content'] = text_content if text_content && !text_content.strip.empty?
|
262
|
+
hash['html_content'] = html_content if html_content && !html_content.strip.empty?
|
263
|
+
hash['template_id'] = template_id if template_id && !template_id.strip.empty?
|
264
|
+
hash['template_data'] = template_data if template_data
|
265
|
+
hash['attachments'] = attachments.map(&:to_hash) unless attachments.empty?
|
266
|
+
hash['headers'] = headers if headers
|
267
|
+
hash['reply_to'] = reply_to.to_hash if reply_to
|
268
|
+
hash['send_time'] = send_time if send_time
|
269
|
+
hash['webhook_data'] = webhook_data if webhook_data
|
270
|
+
hash['tag'] = tag if tag && !tag.strip.empty?
|
271
|
+
hash['tracking'] = tracking.to_hash if tracking
|
272
|
+
|
273
|
+
hash
|
274
|
+
end
|
275
|
+
|
276
|
+
def ==(other)
|
277
|
+
return false unless other.is_a?(Email)
|
278
|
+
|
279
|
+
from == other.from &&
|
280
|
+
to == other.to &&
|
281
|
+
cc == other.cc &&
|
282
|
+
bcc == other.bcc &&
|
283
|
+
subject == other.subject &&
|
284
|
+
text_content == other.text_content &&
|
285
|
+
html_content == other.html_content &&
|
286
|
+
template_id == other.template_id &&
|
287
|
+
template_data == other.template_data &&
|
288
|
+
attachments == other.attachments &&
|
289
|
+
headers == other.headers &&
|
290
|
+
reply_to == other.reply_to &&
|
291
|
+
send_time == other.send_time &&
|
292
|
+
webhook_data == other.webhook_data &&
|
293
|
+
tag == other.tag &&
|
294
|
+
tracking == other.tracking
|
295
|
+
end
|
296
|
+
|
297
|
+
def eql?(other)
|
298
|
+
self == other
|
299
|
+
end
|
300
|
+
|
301
|
+
def hash
|
302
|
+
[from, to, cc, bcc, subject, text_content, html_content, template_id,
|
303
|
+
template_data, attachments, headers, reply_to, send_time, webhook_data, tag, tracking].hash
|
304
|
+
end
|
305
|
+
|
306
|
+
def to_s
|
307
|
+
has_text = text_content && !text_content.empty?
|
308
|
+
has_html = html_content && !html_content.empty?
|
309
|
+
|
310
|
+
<<~STR
|
311
|
+
Email{
|
312
|
+
from=#{from},
|
313
|
+
to=#{to},
|
314
|
+
subject='#{subject}',
|
315
|
+
hasTextContent=#{has_text},
|
316
|
+
hasHtmlContent=#{has_html},
|
317
|
+
templateId='#{template_id}',
|
318
|
+
attachments=#{attachments.size}
|
319
|
+
}
|
320
|
+
STR
|
321
|
+
end
|
322
|
+
|
323
|
+
private
|
324
|
+
|
325
|
+
def validate!
|
326
|
+
raise ValidationException, 'From address is required' if from.nil?
|
327
|
+
|
328
|
+
# Must have at least one recipient
|
329
|
+
if to.empty? && cc.empty? && bcc.empty?
|
330
|
+
raise ValidationException, 'Email must have at least one recipient (to, cc, or bcc)'
|
331
|
+
end
|
332
|
+
|
333
|
+
# Must have either content or template
|
334
|
+
has_content = (text_content && !text_content.strip.empty?) ||
|
335
|
+
(html_content && !html_content.strip.empty?)
|
336
|
+
has_template = template_id && !template_id.strip.empty?
|
337
|
+
|
338
|
+
unless has_content || has_template
|
339
|
+
raise ValidationException, 'Email must have either content (text/HTML) or a template ID'
|
340
|
+
end
|
341
|
+
|
342
|
+
# Validate send time
|
343
|
+
return unless send_time && send_time <= Time.now.to_i
|
344
|
+
|
345
|
+
raise ValidationException, 'Send time must be in the future'
|
346
|
+
end
|
347
|
+
|
348
|
+
# Builder class for creating Email instances
|
349
|
+
class Builder
|
350
|
+
def initialize
|
351
|
+
@to = []
|
352
|
+
@cc = []
|
353
|
+
@bcc = []
|
354
|
+
@attachments = []
|
355
|
+
end
|
356
|
+
|
357
|
+
def from(address)
|
358
|
+
@from = address
|
359
|
+
self
|
360
|
+
end
|
361
|
+
|
362
|
+
def to(address)
|
363
|
+
@to << address
|
364
|
+
self
|
365
|
+
end
|
366
|
+
|
367
|
+
def cc(address)
|
368
|
+
@cc << address
|
369
|
+
self
|
370
|
+
end
|
371
|
+
|
372
|
+
def bcc(address)
|
373
|
+
@bcc << address
|
374
|
+
self
|
375
|
+
end
|
376
|
+
|
377
|
+
def subject(subject)
|
378
|
+
@subject = subject
|
379
|
+
self
|
380
|
+
end
|
381
|
+
|
382
|
+
def text_content(content)
|
383
|
+
@text_content = content
|
384
|
+
self
|
385
|
+
end
|
386
|
+
|
387
|
+
def html_content(content)
|
388
|
+
@html_content = content
|
389
|
+
self
|
390
|
+
end
|
391
|
+
|
392
|
+
def template_id(id)
|
393
|
+
@template_id = id
|
394
|
+
self
|
395
|
+
end
|
396
|
+
|
397
|
+
def template_data(data)
|
398
|
+
@template_data = data
|
399
|
+
self
|
400
|
+
end
|
401
|
+
|
402
|
+
def attachment(attachment)
|
403
|
+
@attachments << attachment
|
404
|
+
self
|
405
|
+
end
|
406
|
+
|
407
|
+
def headers(headers)
|
408
|
+
@headers = headers
|
409
|
+
self
|
410
|
+
end
|
411
|
+
|
412
|
+
def reply_to(address)
|
413
|
+
@reply_to = address
|
414
|
+
self
|
415
|
+
end
|
416
|
+
|
417
|
+
def send_time(time)
|
418
|
+
@send_time = time
|
419
|
+
self
|
420
|
+
end
|
421
|
+
|
422
|
+
def webhook_data(data)
|
423
|
+
@webhook_data = data
|
424
|
+
self
|
425
|
+
end
|
426
|
+
|
427
|
+
def tag(tag)
|
428
|
+
@tag = tag
|
429
|
+
self
|
430
|
+
end
|
431
|
+
|
432
|
+
def tracking(tracking)
|
433
|
+
@tracking = tracking
|
434
|
+
self
|
435
|
+
end
|
436
|
+
|
437
|
+
def build
|
438
|
+
Email.new(self)
|
439
|
+
end
|
440
|
+
end
|
441
|
+
end
|
442
|
+
end
|
@@ -0,0 +1,38 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Laneful
|
4
|
+
# Utility class for verifying webhook signatures
|
5
|
+
class WebhookVerifier
|
6
|
+
ALGORITHM = 'sha256'
|
7
|
+
|
8
|
+
# Verifies a webhook signature
|
9
|
+
def self.verify_signature(secret, payload, signature)
|
10
|
+
return false if secret.nil? || secret.strip.empty?
|
11
|
+
return false if payload.nil?
|
12
|
+
return false if signature.nil? || signature.strip.empty?
|
13
|
+
|
14
|
+
expected_signature = generate_signature(secret, payload)
|
15
|
+
secure_compare?(expected_signature, signature)
|
16
|
+
rescue StandardError
|
17
|
+
false
|
18
|
+
end
|
19
|
+
|
20
|
+
# Generates a signature for the given payload
|
21
|
+
def self.generate_signature(secret, payload)
|
22
|
+
digest = OpenSSL::Digest.new(ALGORITHM)
|
23
|
+
hmac = OpenSSL::HMAC.new(secret, digest)
|
24
|
+
hmac.update(payload)
|
25
|
+
hmac.hexdigest
|
26
|
+
end
|
27
|
+
|
28
|
+
# Compares two strings in constant time to prevent timing attacks
|
29
|
+
def self.secure_compare?(str_a, str_b)
|
30
|
+
return false unless str_a.bytesize == str_b.bytesize
|
31
|
+
|
32
|
+
l = str_a.unpack("C#{str_a.bytesize}")
|
33
|
+
res = 0
|
34
|
+
str_b.each_byte { |byte| res |= byte ^ l.shift }
|
35
|
+
res.zero?
|
36
|
+
end
|
37
|
+
end
|
38
|
+
end
|
data/lib/laneful.rb
ADDED
@@ -0,0 +1,27 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'json'
|
4
|
+
require 'httparty'
|
5
|
+
require 'openssl'
|
6
|
+
require 'base64'
|
7
|
+
|
8
|
+
require_relative 'laneful/version'
|
9
|
+
require_relative 'laneful/exceptions'
|
10
|
+
require_relative 'laneful/models'
|
11
|
+
require_relative 'laneful/client'
|
12
|
+
require_relative 'laneful/webhooks'
|
13
|
+
|
14
|
+
# Laneful Ruby SDK
|
15
|
+
# A modern Ruby client library for the Laneful email API
|
16
|
+
module Laneful
|
17
|
+
class Error < StandardError; end
|
18
|
+
|
19
|
+
# API version
|
20
|
+
API_VERSION = 'v1'
|
21
|
+
|
22
|
+
# Default timeout in seconds
|
23
|
+
DEFAULT_TIMEOUT = 30
|
24
|
+
|
25
|
+
# User agent string
|
26
|
+
USER_AGENT = 'laneful-ruby/1.0.0'
|
27
|
+
end
|
data/scripts/publish.sh
ADDED
@@ -0,0 +1,169 @@
|
|
1
|
+
#!/bin/bash
|
2
|
+
|
3
|
+
# Laneful Ruby SDK Publishing Script
|
4
|
+
# This script handles the complete publishing workflow to RubyGems
|
5
|
+
|
6
|
+
set -e
|
7
|
+
|
8
|
+
# Colors for output
|
9
|
+
RED='\033[0;31m'
|
10
|
+
GREEN='\033[0;32m'
|
11
|
+
YELLOW='\033[1;33m'
|
12
|
+
BLUE='\033[0;34m'
|
13
|
+
NC='\033[0m' # No Color
|
14
|
+
|
15
|
+
# Function to print colored output
|
16
|
+
print_status() {
|
17
|
+
echo -e "${BLUE}[INFO]${NC} $1"
|
18
|
+
}
|
19
|
+
|
20
|
+
print_success() {
|
21
|
+
echo -e "${GREEN}[SUCCESS]${NC} $1"
|
22
|
+
}
|
23
|
+
|
24
|
+
print_warning() {
|
25
|
+
echo -e "${YELLOW}[WARNING]${NC} $1"
|
26
|
+
}
|
27
|
+
|
28
|
+
print_error() {
|
29
|
+
echo -e "${RED}[ERROR]${NC} $1"
|
30
|
+
}
|
31
|
+
|
32
|
+
# Check if required environment variables are set
|
33
|
+
check_environment() {
|
34
|
+
print_status "Checking environment variables..."
|
35
|
+
|
36
|
+
if [ -z "$GEM_HOST_API_KEY" ]; then
|
37
|
+
print_error "GEM_HOST_API_KEY environment variable is not set"
|
38
|
+
print_status "Please set your RubyGems API key:"
|
39
|
+
print_status "export GEM_HOST_API_KEY=your_api_key_here"
|
40
|
+
exit 1
|
41
|
+
fi
|
42
|
+
|
43
|
+
print_success "Environment variables are set"
|
44
|
+
}
|
45
|
+
|
46
|
+
# Check if we're in the right directory
|
47
|
+
check_directory() {
|
48
|
+
if [ ! -f "laneful-ruby.gemspec" ]; then
|
49
|
+
print_error "laneful-ruby.gemspec not found. Please run this script from the project root."
|
50
|
+
exit 1
|
51
|
+
fi
|
52
|
+
print_success "Found gemspec file"
|
53
|
+
}
|
54
|
+
|
55
|
+
# Check Ruby version
|
56
|
+
check_ruby_version() {
|
57
|
+
print_status "Checking Ruby version..."
|
58
|
+
ruby_version=$(ruby -v | cut -d' ' -f2)
|
59
|
+
required_version="3.0.0"
|
60
|
+
|
61
|
+
if [ "$(printf '%s\n' "$required_version" "$ruby_version" | sort -V | head -n1)" = "$required_version" ]; then
|
62
|
+
print_success "Ruby version $ruby_version is compatible"
|
63
|
+
else
|
64
|
+
print_error "Ruby version $ruby_version is not compatible. Required: $required_version or higher"
|
65
|
+
exit 1
|
66
|
+
fi
|
67
|
+
}
|
68
|
+
|
69
|
+
# Install dependencies
|
70
|
+
install_dependencies() {
|
71
|
+
print_status "Installing dependencies..."
|
72
|
+
bundle install
|
73
|
+
if [ $? -eq 0 ]; then
|
74
|
+
print_success "Dependencies installed successfully"
|
75
|
+
else
|
76
|
+
print_error "Failed to install dependencies. Aborting publish."
|
77
|
+
exit 1
|
78
|
+
fi
|
79
|
+
}
|
80
|
+
|
81
|
+
# Run tests
|
82
|
+
run_tests() {
|
83
|
+
print_status "Running tests..."
|
84
|
+
bundle exec rspec
|
85
|
+
if [ $? -eq 0 ]; then
|
86
|
+
print_success "All tests passed"
|
87
|
+
else
|
88
|
+
print_error "Tests failed. Aborting publish."
|
89
|
+
exit 1
|
90
|
+
fi
|
91
|
+
}
|
92
|
+
|
93
|
+
# Run linting
|
94
|
+
run_lint() {
|
95
|
+
print_status "Running linter..."
|
96
|
+
bundle exec rubocop
|
97
|
+
if [ $? -eq 0 ]; then
|
98
|
+
print_success "Linting passed"
|
99
|
+
else
|
100
|
+
print_error "Linting failed. Aborting publish."
|
101
|
+
exit 1
|
102
|
+
fi
|
103
|
+
}
|
104
|
+
|
105
|
+
# Build the gem
|
106
|
+
build_gem() {
|
107
|
+
print_status "Building gem..."
|
108
|
+
gem build laneful-ruby.gemspec
|
109
|
+
if [ $? -eq 0 ]; then
|
110
|
+
print_success "Gem built successfully"
|
111
|
+
else
|
112
|
+
print_error "Gem build failed. Aborting publish."
|
113
|
+
exit 1
|
114
|
+
fi
|
115
|
+
}
|
116
|
+
|
117
|
+
# Publish to RubyGems
|
118
|
+
publish_gem() {
|
119
|
+
print_status "Publishing to RubyGems..."
|
120
|
+
gem push laneful-ruby-*.gem
|
121
|
+
if [ $? -eq 0 ]; then
|
122
|
+
print_success "Gem published successfully to RubyGems!"
|
123
|
+
else
|
124
|
+
print_error "Failed to publish gem to RubyGems."
|
125
|
+
exit 1
|
126
|
+
fi
|
127
|
+
}
|
128
|
+
|
129
|
+
# Clean up
|
130
|
+
cleanup() {
|
131
|
+
print_status "Cleaning up..."
|
132
|
+
rm -f laneful-ruby-*.gem
|
133
|
+
print_success "Cleanup completed"
|
134
|
+
}
|
135
|
+
|
136
|
+
# Main execution
|
137
|
+
main() {
|
138
|
+
print_status "Starting Laneful Ruby SDK publishing process..."
|
139
|
+
|
140
|
+
check_environment
|
141
|
+
check_directory
|
142
|
+
check_ruby_version
|
143
|
+
install_dependencies
|
144
|
+
|
145
|
+
# Ask for confirmation
|
146
|
+
echo
|
147
|
+
print_warning "This will publish the gem to RubyGems. Are you sure? (y/N)"
|
148
|
+
read -r response
|
149
|
+
if [[ ! "$response" =~ ^[Yy]$ ]]; then
|
150
|
+
print_status "Publishing cancelled."
|
151
|
+
exit 0
|
152
|
+
fi
|
153
|
+
|
154
|
+
run_tests
|
155
|
+
run_lint
|
156
|
+
build_gem
|
157
|
+
publish_gem
|
158
|
+
cleanup
|
159
|
+
|
160
|
+
print_success "Publishing process completed successfully!"
|
161
|
+
print_status "Your gem is now available on RubyGems: https://rubygems.org/gems/laneful-ruby"
|
162
|
+
}
|
163
|
+
|
164
|
+
# Handle script interruption
|
165
|
+
trap cleanup EXIT
|
166
|
+
|
167
|
+
# Run main function
|
168
|
+
main "$@"
|
169
|
+
|