sevk 0.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.
checksums.yaml ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA256:
3
+ metadata.gz: 6c7f36600a0d24e9b0125300b35f125d85c3c2d116902386c4336c1969c539b3
4
+ data.tar.gz: 99ca2a8b06fba11e1d604ea021cd8664b5cd5cd48a01eaef4a0fd38219acf9d2
5
+ SHA512:
6
+ metadata.gz: ede0ed66474c75ed2544e0e5c1d41013946af97d81728ecdccedc7b1022a2d9ccd5bc8a47f67c99bc5b8a1038f4bb1fb84983024a12d8f08e070565f61bcb70b
7
+ data.tar.gz: eab455f069c6d246b19385ca7842c66f14d16781fd837d0c5d05c2bcf894973b81eee0ff1d96886c1a4a2e4c6b9b6cad18062ca8a5c87b26c5a2937f2923fbfb
data/README.md ADDED
@@ -0,0 +1,75 @@
1
+ > [!WARNING]
2
+ > Sevk is currently in private beta. This SDK is not yet available for public use.
3
+ > Join the waitlist at [sevk.io](https://sevk.io) to get early access.
4
+
5
+ <p align="center">
6
+ <img src="https://sevk.io/logo.png" alt="Sevk" width="120" />
7
+ </p>
8
+
9
+ <h1 align="center">Sevk Ruby SDK</h1>
10
+
11
+ <p align="center">
12
+ Official Ruby SDK for <a href="https://sevk.io">Sevk</a> email platform.
13
+ </p>
14
+
15
+ <p align="center">
16
+ <a href="https://docs.sevk.io">Documentation</a> •
17
+ <a href="https://sevk.io">Website</a>
18
+ </p>
19
+
20
+ ## Installation
21
+
22
+ ```bash
23
+ gem install sevk
24
+ ```
25
+
26
+ ## Send Email
27
+
28
+ ```ruby
29
+ require 'sevk'
30
+
31
+ client = Sevk::Client.new('your-api-key')
32
+
33
+ client.emails.send(
34
+ to: 'recipient@example.com',
35
+ from: 'hello@yourdomain.com',
36
+ subject: 'Hello from Sevk!',
37
+ html: '<h1>Welcome!</h1>'
38
+ )
39
+ ```
40
+
41
+ ## Send Email with Markup
42
+
43
+ ```ruby
44
+ require 'sevk'
45
+
46
+ client = Sevk::Client.new('your-api-key')
47
+
48
+ html = Sevk::Markup.render(<<~MARKUP
49
+ <section padding="40px 20px" background-color="#f8f9fa">
50
+ <container max-width="600px">
51
+ <heading level="1" color="#1a1a1a">Welcome!</heading>
52
+ <paragraph color="#666666">Thanks for signing up.</paragraph>
53
+ <button href="https://example.com" background-color="#5227FF" color="#ffffff" padding="12px 24px">
54
+ Get Started
55
+ </button>
56
+ </container>
57
+ </section>
58
+ MARKUP
59
+ )
60
+
61
+ client.emails.send(
62
+ to: 'recipient@example.com',
63
+ from: 'hello@yourdomain.com',
64
+ subject: 'Welcome!',
65
+ html: html
66
+ )
67
+ ```
68
+
69
+ ## Documentation
70
+
71
+ For full documentation, visit [docs.sevk.io](https://docs.sevk.io)
72
+
73
+ ## License
74
+
75
+ MIT
data/Rakefile ADDED
@@ -0,0 +1,42 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "bundler/gem_tasks"
4
+ require "rspec/core/rake_task"
5
+
6
+ RSpec::Core::RakeTask.new(:spec)
7
+
8
+ task default: :spec
9
+
10
+ desc "Update all dependencies to latest versions"
11
+ task :update do
12
+ sh "bundle update"
13
+ end
14
+
15
+ desc "Update dependencies and show outdated gems"
16
+ task :outdated do
17
+ sh "bundle outdated"
18
+ end
19
+
20
+ desc "Run tests"
21
+ task :test => :spec
22
+
23
+ desc "Build the gem"
24
+ task :build do
25
+ sh "gem build sevk.gemspec"
26
+ end
27
+
28
+ desc "Install the gem locally"
29
+ task :install => :build do
30
+ sh "gem install sevk-*.gem"
31
+ end
32
+
33
+ desc "Release the gem to RubyGems"
34
+ task :release => :build do
35
+ sh "gem push sevk-*.gem"
36
+ end
37
+
38
+ desc "Clean up build artifacts"
39
+ task :clean do
40
+ FileUtils.rm_f Dir.glob("sevk-*.gem")
41
+ FileUtils.rm_rf "pkg"
42
+ end
@@ -0,0 +1,164 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Sevk
4
+ class Client
5
+ attr_reader :api_key, :base_url
6
+
7
+ def initialize(api_key:, base_url: nil)
8
+ @api_key = api_key
9
+ @base_url = base_url || DEFAULT_BASE_URL
10
+ end
11
+
12
+ def contacts
13
+ @contacts ||= Resources::Contacts.new(self)
14
+ end
15
+
16
+ def audiences
17
+ @audiences ||= Resources::Audiences.new(self)
18
+ end
19
+
20
+ def templates
21
+ @templates ||= Resources::Templates.new(self)
22
+ end
23
+
24
+ def broadcasts
25
+ @broadcasts ||= Resources::Broadcasts.new(self)
26
+ end
27
+
28
+ def domains
29
+ @domains ||= Resources::Domains.new(self)
30
+ end
31
+
32
+ def topics
33
+ @topics ||= Resources::Topics.new(self)
34
+ end
35
+
36
+ def segments
37
+ @segments ||= Resources::Segments.new(self)
38
+ end
39
+
40
+ def subscriptions
41
+ @subscriptions ||= Resources::Subscriptions.new(self)
42
+ end
43
+
44
+ def emails
45
+ @emails ||= Resources::Emails.new(self)
46
+ end
47
+
48
+ def get(path, params = {})
49
+ request(:get, path, params)
50
+ end
51
+
52
+ def post(path, body = {})
53
+ request(:post, path, body)
54
+ end
55
+
56
+ def patch(path, body = {})
57
+ request(:patch, path, body)
58
+ end
59
+
60
+ def put(path, body = {})
61
+ request(:put, path, body)
62
+ end
63
+
64
+ def delete(path)
65
+ request(:delete, path)
66
+ end
67
+
68
+ private
69
+
70
+ def connection
71
+ @connection ||= Faraday.new do |conn|
72
+ conn.request :json
73
+ conn.response :json, content_type: /\bjson$/
74
+ conn.request :retry, max: 2, interval: 0.5, backoff_factor: 2
75
+ conn.headers["Authorization"] = "Bearer #{api_key}"
76
+ conn.headers["Content-Type"] = "application/json"
77
+ conn.headers["Accept"] = "application/json"
78
+ conn.adapter Faraday.default_adapter
79
+ end
80
+ end
81
+
82
+ def build_url(path)
83
+ # Ensure base_url ends without slash and path starts with slash
84
+ base = base_url.chomp("/")
85
+ path = "/#{path}" unless path.start_with?("/")
86
+ "#{base}#{path}"
87
+ end
88
+
89
+ def request(method, path, body = nil)
90
+ url = build_url(path)
91
+ response = case method
92
+ when :get
93
+ connection.get(url, body)
94
+ when :post
95
+ connection.post(url, body)
96
+ when :patch
97
+ connection.patch(url, body)
98
+ when :put
99
+ connection.put(url, body)
100
+ when :delete
101
+ connection.delete(url)
102
+ end
103
+
104
+ handle_response(response)
105
+ end
106
+
107
+ def handle_response(response)
108
+ case response.status
109
+ when 200..299
110
+ response.body
111
+ when 400
112
+ raise ValidationError.new(
113
+ error_message(response),
114
+ status_code: response.status,
115
+ response: response.body
116
+ )
117
+ when 401
118
+ raise AuthenticationError.new(
119
+ error_message(response),
120
+ status_code: response.status,
121
+ response: response.body
122
+ )
123
+ when 403
124
+ raise Error.new(
125
+ error_message(response),
126
+ status_code: response.status,
127
+ response: response.body
128
+ )
129
+ when 404
130
+ raise NotFoundError.new(
131
+ error_message(response),
132
+ status_code: response.status,
133
+ response: response.body
134
+ )
135
+ when 429
136
+ raise RateLimitError.new(
137
+ error_message(response),
138
+ status_code: response.status,
139
+ response: response.body
140
+ )
141
+ when 500..599
142
+ raise ServerError.new(
143
+ error_message(response),
144
+ status_code: response.status,
145
+ response: response.body
146
+ )
147
+ else
148
+ raise Error.new(
149
+ error_message(response),
150
+ status_code: response.status,
151
+ response: response.body
152
+ )
153
+ end
154
+ end
155
+
156
+ def error_message(response)
157
+ if response.body.is_a?(Hash) && response.body["message"]
158
+ "#{response.status}: #{response.body['message']}"
159
+ else
160
+ "#{response.status}: #{response.body}"
161
+ end
162
+ end
163
+ end
164
+ end
data/lib/sevk/error.rb ADDED
@@ -0,0 +1,40 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Sevk
4
+ class Error < StandardError
5
+ attr_reader :status_code, :code, :response
6
+
7
+ def initialize(message = nil, status_code: nil, code: nil, response: nil)
8
+ @status_code = status_code
9
+ @code = code
10
+ @response = response
11
+ super(message)
12
+ end
13
+
14
+ def not_found?
15
+ status_code == 404
16
+ end
17
+
18
+ def unauthorized?
19
+ status_code == 401
20
+ end
21
+
22
+ def forbidden?
23
+ status_code == 403
24
+ end
25
+
26
+ def bad_request?
27
+ status_code == 400
28
+ end
29
+
30
+ def server_error?
31
+ status_code.to_i >= 500 && status_code.to_i < 600
32
+ end
33
+ end
34
+
35
+ class AuthenticationError < Error; end
36
+ class NotFoundError < Error; end
37
+ class ValidationError < Error; end
38
+ class RateLimitError < Error; end
39
+ class ServerError < Error; end
40
+ end