opensend 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: 349126cf4fb71e6ae38091a0ca4e7a800031bbcf11b0284f033206fd16331f67
4
+ data.tar.gz: 01b7aec53d77983da3e737bed3f3575f45871eca0ce9f03dc7eaf18d92920fd9
5
+ SHA512:
6
+ metadata.gz: a6ab3ec4098d2b45771e13ac5fa830eb8d910a5142591496fceb2c49f1da693c26441bc0b826730e6a88f2024d956278dab3d8532a3a70b3e745b2a5d49ae2a4
7
+ data.tar.gz: 657a432a912b857007d05b8cd133ad73b69e7f07016642fb877d5a2acfcc0daf1401cb4ae9c24ef59658bbdbd3cffcccbe74ee4363a1867ca39377f57f114989
data/README.md ADDED
@@ -0,0 +1,124 @@
1
+ # opensend Ruby SDK
2
+
3
+ Minimal first-party Ruby SDK for OpenSend transactional email sends with a
4
+ familiar API surface.
5
+
6
+ Use your OpenSend API key (`os_...`) with OpenSend's Ruby API surface.
7
+ Existing Resend-style send calls can migrate through the alias documented
8
+ below.
9
+
10
+ ## Installation
11
+
12
+ This package is ready for RubyGems publishing. Until the RubyGems publish is
13
+ complete, install the built gem from this repository:
14
+
15
+ ```bash
16
+ cd packages/ruby-sdk
17
+ gem build opensend.gemspec
18
+ gem install ./opensend-0.1.0.gem
19
+ ```
20
+
21
+ After `opensend` is published to RubyGems, install it with:
22
+
23
+ ```bash
24
+ gem install opensend
25
+ ```
26
+
27
+ ## Setup
28
+
29
+ Use an environment variable instead of hardcoding API keys:
30
+
31
+ ```bash
32
+ export OPENSEND_API_KEY="os_your_api_key"
33
+ ```
34
+
35
+ For self-hosted OpenSend, point the SDK at your deployment origin. The default
36
+ hosted origin is `https://opensend.namuh.co`.
37
+
38
+ ```bash
39
+ export OPENSEND_BASE_URL="http://localhost:3015"
40
+ ```
41
+
42
+ ## Send an email
43
+
44
+ The module-level surface uses OpenSend while keeping familiar Ruby email-send
45
+ ergonomics:
46
+
47
+ ```ruby
48
+ require "opensend"
49
+
50
+ OpenSend.api_key ENV.fetch("OPENSEND_API_KEY")
51
+ OpenSend.base_url ENV.fetch("OPENSEND_BASE_URL", OpenSend::DEFAULT_BASE_URL)
52
+
53
+ email = OpenSend::Emails.send(
54
+ from: "hello@yourdomain.com",
55
+ to: "recipient@example.com",
56
+ subject: "Hello from OpenSend",
57
+ html: "<h1>It works!</h1>"
58
+ )
59
+
60
+ puts email.fetch("id")
61
+ ```
62
+
63
+ `Resend` is also exported as an alias constant for existing send code migrating
64
+ to OpenSend:
65
+
66
+ ```ruby
67
+ require "opensend"
68
+
69
+ Resend.api_key ENV.fetch("OPENSEND_API_KEY")
70
+ email = Resend::Emails.send(
71
+ from: "hello@yourdomain.com",
72
+ to: ["recipient@example.com"],
73
+ subject: "Hello from OpenSend",
74
+ text: "It works!"
75
+ )
76
+ ```
77
+
78
+ ## Instance client
79
+
80
+ ```ruby
81
+ client = OpenSend::Client.new(
82
+ api_key: ENV.fetch("OPENSEND_API_KEY"),
83
+ base_url: ENV.fetch("OPENSEND_BASE_URL", OpenSend::DEFAULT_BASE_URL)
84
+ )
85
+
86
+ email = client.emails.send(
87
+ from: "hello@yourdomain.com",
88
+ to: "recipient@example.com",
89
+ subject: "Hello from OpenSend",
90
+ html: "<h1>It works!</h1>"
91
+ )
92
+ ```
93
+
94
+ ## Errors
95
+
96
+ Non-2xx API responses raise `OpenSend::Error` (also exported as
97
+ `OpenSend::APIError`). The exception keeps OpenSend's public error envelope
98
+ fields when present.
99
+
100
+ ```ruby
101
+ begin
102
+ OpenSend::Emails.send(params)
103
+ rescue OpenSend::Error => error
104
+ warn [error.status_code, error.code, error.message, error.details].inspect
105
+ end
106
+ ```
107
+
108
+ ## Supported first slice
109
+
110
+ - `OpenSend::Emails.send(params)` → `POST /emails`
111
+ - `OpenSend::Client#emails.send(params)` → `POST /emails`
112
+ - Bearer API key auth
113
+ - Configurable base URL
114
+ - Structured API error envelope parsing
115
+ - Resend alias constant for existing send code migrating to OpenSend
116
+
117
+ This first package intentionally does not implement batch sends, async jobs,
118
+ attachments helpers, or full resource-surface parity yet.
119
+
120
+ ## Tests
121
+
122
+ ```bash
123
+ ruby -I packages/ruby-sdk/lib packages/ruby-sdk/test/opensend_test.rb
124
+ ```
@@ -0,0 +1,5 @@
1
+ # frozen_string_literal: true
2
+
3
+ module OpenSend
4
+ VERSION = "0.1.0"
5
+ end
data/lib/opensend.rb ADDED
@@ -0,0 +1,186 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "json"
4
+ require "net/http"
5
+ require "uri"
6
+
7
+ require_relative "opensend/version"
8
+
9
+ module OpenSend
10
+ DEFAULT_BASE_URL = "https://opensend.namuh.co"
11
+ USER_AGENT = "opensend-ruby/#{VERSION}"
12
+
13
+ class << self
14
+ attr_writer :api_key, :base_url
15
+
16
+ def api_key(value = nil)
17
+ @api_key = value unless value.nil?
18
+ @api_key
19
+ end
20
+
21
+ def base_url(value = nil)
22
+ @base_url = value unless value.nil?
23
+ @base_url || DEFAULT_BASE_URL
24
+ end
25
+
26
+ def emails
27
+ EmailsResource.new(Client.new(api_key: configured_api_key, base_url: base_url))
28
+ end
29
+
30
+ private
31
+
32
+ def configured_api_key
33
+ key = api_key
34
+ raise ArgumentError, "set OpenSend.api_key before making API requests" if key.nil? || key.to_s.strip.empty?
35
+
36
+ key
37
+ end
38
+ end
39
+
40
+ class Error < StandardError
41
+ attr_reader :status_code, :name, :code, :details, :body
42
+
43
+ def initialize(message, status_code:, name: nil, code: nil, details: nil, body: nil)
44
+ super(message)
45
+ @status_code = status_code
46
+ @name = name
47
+ @code = code
48
+ @details = details
49
+ @body = body
50
+ end
51
+ end
52
+
53
+ APIError = Error
54
+
55
+ class Client
56
+ attr_reader :api_key, :base_url
57
+
58
+ def initialize(api_key:, base_url: DEFAULT_BASE_URL)
59
+ @api_key = normalize_api_key(api_key)
60
+ @base_uri = normalize_base_url(base_url)
61
+ @base_url = @base_uri.to_s
62
+ end
63
+
64
+ def emails
65
+ EmailsResource.new(self)
66
+ end
67
+
68
+ def post(path, payload)
69
+ uri = endpoint(path)
70
+ request = Net::HTTP::Post.new(uri)
71
+ request["Authorization"] = "Bearer #{api_key}"
72
+ request["Content-Type"] = "application/json"
73
+ request["Accept"] = "application/json"
74
+ request["User-Agent"] = USER_AGENT
75
+ request.body = JSON.generate(payload)
76
+
77
+ response = perform_request(uri, request)
78
+ body = response.body.to_s
79
+
80
+ unless response.is_a?(Net::HTTPSuccess)
81
+ raise api_error(response, body)
82
+ end
83
+
84
+ body.empty? ? {} : JSON.parse(body)
85
+ rescue JSON::ParserError => error
86
+ raise Error.new(
87
+ "Invalid JSON response from OpenSend",
88
+ status_code: 0,
89
+ name: "parse_error",
90
+ code: "parse_error",
91
+ body: error.message
92
+ )
93
+ rescue IOError, SystemCallError, Timeout::Error, SocketError => error
94
+ raise Error.new(
95
+ error.message,
96
+ status_code: 0,
97
+ name: "request_error",
98
+ code: "request_error"
99
+ )
100
+ end
101
+
102
+ private
103
+
104
+ def normalize_api_key(value)
105
+ key = value.to_s.strip
106
+ raise ArgumentError, "API key is required" if key.empty?
107
+
108
+ key
109
+ end
110
+
111
+ def normalize_base_url(value)
112
+ raw = value.to_s.strip
113
+ raise ArgumentError, "base URL must be a non-empty string" if raw.empty?
114
+
115
+ uri = URI.parse(raw)
116
+ unless uri.is_a?(URI::HTTP) && uri.host
117
+ raise ArgumentError, "base URL must be a valid absolute http or https URL"
118
+ end
119
+
120
+ uri.path = uri.path.to_s.sub(%r{/+\z}, "")
121
+ uri.query = nil
122
+ uri.fragment = nil
123
+ uri
124
+ rescue URI::InvalidURIError
125
+ raise ArgumentError, "base URL must be a valid absolute http or https URL"
126
+ end
127
+
128
+ def endpoint(path)
129
+ uri = @base_uri.dup
130
+ base_path = uri.path.to_s.sub(%r{/+\z}, "")
131
+ request_path = "/#{path.to_s.sub(%r{\A/+}, "")}"
132
+ uri.path = base_path.empty? ? request_path : "#{base_path}#{request_path}"
133
+ uri.query = nil
134
+ uri.fragment = nil
135
+ uri
136
+ end
137
+
138
+ def perform_request(uri, request)
139
+ Net::HTTP.start(uri.host, uri.port, use_ssl: uri.scheme == "https") do |http|
140
+ http.request(request)
141
+ end
142
+ end
143
+
144
+ def api_error(response, body)
145
+ envelope = parse_json_object(body)
146
+ message = envelope["message"] || response.message || "OpenSend API request failed"
147
+
148
+ Error.new(
149
+ message,
150
+ status_code: response.code.to_i,
151
+ name: envelope["name"],
152
+ code: envelope["code"],
153
+ details: envelope["details"],
154
+ body: body
155
+ )
156
+ end
157
+
158
+ def parse_json_object(body)
159
+ parsed = body.empty? ? {} : JSON.parse(body)
160
+ parsed.is_a?(Hash) ? parsed : {}
161
+ rescue JSON::ParserError
162
+ {}
163
+ end
164
+ end
165
+
166
+ class EmailsResource
167
+ def initialize(client)
168
+ @client = client
169
+ end
170
+
171
+ def send(params)
172
+ @client.post("/emails", params)
173
+ end
174
+ end
175
+
176
+ module Emails
177
+ def self.send(params, api_key: nil, base_url: nil)
178
+ key = api_key || OpenSend.api_key
179
+ raise ArgumentError, "set OpenSend.api_key before making API requests" if key.nil? || key.to_s.strip.empty?
180
+
181
+ Client.new(api_key: key, base_url: base_url || OpenSend.base_url).emails.send(params)
182
+ end
183
+ end
184
+ end
185
+
186
+ Resend = OpenSend unless defined?(Resend)
data/opensend.gemspec ADDED
@@ -0,0 +1,25 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "lib/opensend/version"
4
+
5
+ Gem::Specification.new do |spec|
6
+ spec.name = "opensend"
7
+ spec.version = OpenSend::VERSION
8
+ spec.summary = "Ruby SDK for the OpenSend email API"
9
+ spec.description = "Minimal first-party Ruby SDK for OpenSend transactional email sends with a familiar API surface."
10
+ spec.authors = ["OpenSend"]
11
+ spec.homepage = "https://github.com/namuh-eng/opensend"
12
+ # RubyGems 3.0 does not recognize Elastic-2.0 as SPDX, so keep builds warning-free
13
+ # while linking to the repository license below.
14
+ spec.license = "Nonstandard"
15
+ spec.required_ruby_version = ">= 2.6"
16
+ spec.metadata = {
17
+ "homepage_uri" => spec.homepage,
18
+ "source_code_uri" => "https://github.com/namuh-eng/opensend",
19
+ "bug_tracker_uri" => "https://github.com/namuh-eng/opensend/issues",
20
+ "license_uri" => "https://github.com/namuh-eng/opensend/blob/main/LICENSE"
21
+ }
22
+
23
+ spec.files = Dir["lib/**/*.rb", "README.md", "opensend.gemspec"]
24
+ spec.require_paths = ["lib"]
25
+ end
metadata ADDED
@@ -0,0 +1,51 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: opensend
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.1.0
5
+ platform: ruby
6
+ authors:
7
+ - OpenSend
8
+ autorequire:
9
+ bindir: bin
10
+ cert_chain: []
11
+ date: 2026-05-23 00:00:00.000000000 Z
12
+ dependencies: []
13
+ description: Minimal first-party Ruby SDK for OpenSend transactional email sends with
14
+ a familiar API surface.
15
+ email:
16
+ executables: []
17
+ extensions: []
18
+ extra_rdoc_files: []
19
+ files:
20
+ - README.md
21
+ - lib/opensend.rb
22
+ - lib/opensend/version.rb
23
+ - opensend.gemspec
24
+ homepage: https://github.com/namuh-eng/opensend
25
+ licenses:
26
+ - Nonstandard
27
+ metadata:
28
+ homepage_uri: https://github.com/namuh-eng/opensend
29
+ source_code_uri: https://github.com/namuh-eng/opensend
30
+ bug_tracker_uri: https://github.com/namuh-eng/opensend/issues
31
+ license_uri: https://github.com/namuh-eng/opensend/blob/main/LICENSE
32
+ post_install_message:
33
+ rdoc_options: []
34
+ require_paths:
35
+ - lib
36
+ required_ruby_version: !ruby/object:Gem::Requirement
37
+ requirements:
38
+ - - ">="
39
+ - !ruby/object:Gem::Version
40
+ version: '2.6'
41
+ required_rubygems_version: !ruby/object:Gem::Requirement
42
+ requirements:
43
+ - - ">="
44
+ - !ruby/object:Gem::Version
45
+ version: '0'
46
+ requirements: []
47
+ rubygems_version: 3.0.3.1
48
+ signing_key:
49
+ specification_version: 4
50
+ summary: Ruby SDK for the OpenSend email API
51
+ test_files: []