poodle-ruby 1.0.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.
@@ -0,0 +1,64 @@
1
+ #!/usr/bin/env ruby
2
+ # frozen_string_literal: true
3
+
4
+ require "bundler/setup"
5
+ require "poodle"
6
+
7
+ # Basic usage example for the Poodle Ruby SDK
8
+
9
+ # Set your API key (you can also use environment variables)
10
+ # ENV["POODLE_API_KEY"] = "your_api_key_here"
11
+
12
+ begin
13
+ # Initialize the client
14
+ client = Poodle::Client.new(api_key: "your_api_key_here")
15
+
16
+ # Send a simple HTML email
17
+ response = client.send(
18
+ from: "sender@example.com",
19
+ to: "recipient@example.com",
20
+ subject: "Hello from Poodle Ruby SDK!",
21
+ html: "<h1>Hello World!</h1><p>This email was sent using the Poodle Ruby SDK.</p>"
22
+ )
23
+
24
+ if response.success?
25
+ puts "✅ Email sent successfully!"
26
+ else
27
+ puts "❌ Failed to send email"
28
+ end
29
+ puts "Message: #{response.message}"
30
+
31
+ # Send a text-only email
32
+ text_response = client.send_text(
33
+ from: "sender@example.com",
34
+ to: "recipient@example.com",
35
+ subject: "Plain text email",
36
+ text: "This is a plain text email sent using the Poodle Ruby SDK."
37
+ )
38
+
39
+ puts "\nText email result: #{text_response.success? ? 'Success' : 'Failed'}"
40
+
41
+ # Send an HTML-only email
42
+ html_response = client.send_html(
43
+ from: "sender@example.com",
44
+ to: "recipient@example.com",
45
+ subject: "HTML email",
46
+ html: "<h2>Newsletter</h2><p>This is an HTML email with <strong>formatting</strong>.</p>"
47
+ )
48
+
49
+ puts "HTML email result: #{html_response.success? ? 'Success' : 'Failed'}"
50
+ rescue Poodle::ValidationError => e
51
+ puts "❌ Validation error: #{e.message}"
52
+ puts "Errors: #{e.errors}"
53
+ rescue Poodle::AuthenticationError => e
54
+ puts "❌ Authentication error: #{e.message}"
55
+ puts "Please check your API key"
56
+ rescue Poodle::RateLimitError => e
57
+ puts "❌ Rate limit exceeded: #{e.message}"
58
+ puts "Retry after: #{e.retry_after} seconds" if e.retry_after
59
+ rescue Poodle::Error => e
60
+ puts "❌ Poodle error: #{e.message}"
61
+ puts "Status code: #{e.status_code}" if e.status_code
62
+ rescue StandardError => e
63
+ puts "❌ Unexpected error: #{e.message}"
64
+ end
@@ -0,0 +1,222 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "configuration"
4
+ require_relative "http_client"
5
+ require_relative "email"
6
+ require_relative "email_response"
7
+ require_relative "errors/validation_error"
8
+
9
+ module Poodle
10
+ # Main Poodle SDK client for sending emails
11
+ #
12
+ # @example Basic usage
13
+ # client = Poodle::Client.new(api_key: "your_api_key")
14
+ # response = client.send(
15
+ # from: "sender@example.com",
16
+ # to: "recipient@example.com",
17
+ # subject: "Hello World",
18
+ # html: "<h1>Hello!</h1>"
19
+ # )
20
+ #
21
+ # @example Using configuration object
22
+ # config = Poodle::Configuration.new(
23
+ # api_key: "your_api_key",
24
+ # debug: true
25
+ # )
26
+ # client = Poodle::Client.new(config)
27
+ #
28
+ # @example Using environment variables
29
+ # ENV["POODLE_API_KEY"] = "your_api_key"
30
+ # client = Poodle::Client.new
31
+ class Client
32
+ # @return [Configuration] the configuration object
33
+ attr_reader :config
34
+
35
+ # @return [HttpClient] the HTTP client
36
+ attr_reader :http_client
37
+
38
+ # Initialize a new Client
39
+ #
40
+ # @param config_or_api_key [Configuration, String, nil] configuration object or API key
41
+ # @param base_url [String, nil] base URL (only used if first param is API key)
42
+ # @param timeout [Integer, nil] request timeout (only used if first param is API key)
43
+ # @param connect_timeout [Integer, nil] connection timeout (only used if first param is API key)
44
+ # @param debug [Boolean] enable debug mode (only used if first param is API key)
45
+ # @param http_options [Hash] additional HTTP options (only used if first param is API key)
46
+ #
47
+ # @example With API key
48
+ # client = Poodle::Client.new("your_api_key")
49
+ #
50
+ # @example With configuration
51
+ # config = Poodle::Configuration.new(api_key: "your_api_key")
52
+ # client = Poodle::Client.new(config)
53
+ #
54
+ # @example With keyword arguments
55
+ # client = Poodle::Client.new(
56
+ # api_key: "your_api_key",
57
+ # debug: true,
58
+ # timeout: 60
59
+ # )
60
+ def initialize(config_or_api_key = nil, **options)
61
+ @config = case config_or_api_key
62
+ when Configuration
63
+ config_or_api_key
64
+ when String
65
+ Configuration.new(
66
+ api_key: config_or_api_key,
67
+ base_url: options[:base_url],
68
+ timeout: options[:timeout],
69
+ connect_timeout: options[:connect_timeout],
70
+ debug: options.fetch(:debug, false),
71
+ http_options: options.fetch(:http_options, {})
72
+ )
73
+ when nil
74
+ # Support keyword arguments for convenience
75
+ Configuration.new(**options)
76
+ else
77
+ raise ArgumentError, "Expected Configuration object, API key string, or nil"
78
+ end
79
+
80
+ @http_client = HttpClient.new(@config)
81
+ end
82
+
83
+ # Send an email using an Email object or hash
84
+ #
85
+ # @param email [Email, Hash] the email to send
86
+ # @return [EmailResponse] the response from the API
87
+ # @raise [ValidationError] if email data is invalid
88
+ # @raise [Poodle::Error] if the request fails
89
+ #
90
+ # @example With Email object
91
+ # email = Poodle::Email.new(
92
+ # from: "sender@example.com",
93
+ # to: "recipient@example.com",
94
+ # subject: "Hello",
95
+ # html: "<h1>Hello!</h1>"
96
+ # )
97
+ # response = client.send_email(email)
98
+ #
99
+ # @example With hash
100
+ # response = client.send_email({
101
+ # from: "sender@example.com",
102
+ # to: "recipient@example.com",
103
+ # subject: "Hello",
104
+ # text: "Hello!"
105
+ # })
106
+ def send_email(email)
107
+ email_obj = email.is_a?(Email) ? email : create_email_from_hash(email)
108
+
109
+ response_data = @http_client.post("v1/send-email", email_obj.to_h)
110
+ EmailResponse.from_api_response(response_data)
111
+ end
112
+
113
+ # Send an email with individual parameters
114
+ #
115
+ # @param from [String] sender email address
116
+ # @param to [String] recipient email address
117
+ # @param subject [String] email subject
118
+ # @param html [String, nil] HTML content
119
+ # @param text [String, nil] plain text content
120
+ # @return [EmailResponse] the response from the API
121
+ # @raise [ValidationError] if email data is invalid
122
+ # @raise [Poodle::Error] if the request fails
123
+ #
124
+ # @example
125
+ # response = client.send(
126
+ # from: "sender@example.com",
127
+ # to: "recipient@example.com",
128
+ # subject: "Hello World",
129
+ # html: "<h1>Hello!</h1>",
130
+ # text: "Hello!"
131
+ # )
132
+ def send(from:, to:, subject:, html: nil, text: nil)
133
+ email = Email.new(from: from, to: to, subject: subject, html: html, text: text)
134
+ send_email(email)
135
+ end
136
+
137
+ # Send an HTML email
138
+ #
139
+ # @param from [String] sender email address
140
+ # @param to [String] recipient email address
141
+ # @param subject [String] email subject
142
+ # @param html [String] HTML content
143
+ # @return [EmailResponse] the response from the API
144
+ # @raise [ValidationError] if email data is invalid
145
+ # @raise [Poodle::Error] if the request fails
146
+ #
147
+ # @example
148
+ # response = client.send_html(
149
+ # from: "sender@example.com",
150
+ # to: "recipient@example.com",
151
+ # subject: "Newsletter",
152
+ # html: "<h1>Newsletter</h1><p>Content here</p>"
153
+ # )
154
+ def send_html(from:, to:, subject:, html:)
155
+ send(from: from, to: to, subject: subject, html: html)
156
+ end
157
+
158
+ # Send a plain text email
159
+ #
160
+ # @param from [String] sender email address
161
+ # @param to [String] recipient email address
162
+ # @param subject [String] email subject
163
+ # @param text [String] plain text content
164
+ # @return [EmailResponse] the response from the API
165
+ # @raise [ValidationError] if email data is invalid
166
+ # @raise [Poodle::Error] if the request fails
167
+ #
168
+ # @example
169
+ # response = client.send_text(
170
+ # from: "sender@example.com",
171
+ # to: "recipient@example.com",
172
+ # subject: "Simple notification",
173
+ # text: "This is a simple text notification."
174
+ # )
175
+ def send_text(from:, to:, subject:, text:)
176
+ send(from: from, to: to, subject: subject, text: text)
177
+ end
178
+
179
+ # Get the SDK version
180
+ #
181
+ # @return [String] the SDK version
182
+ def version
183
+ @config.class::SDK_VERSION
184
+ end
185
+
186
+ private
187
+
188
+ # Create an Email object from hash data
189
+ #
190
+ # @param data [Hash] email data
191
+ # @return [Email] the email object
192
+ # @raise [ValidationError] if required fields are missing
193
+ def create_email_from_hash(data)
194
+ validate_required_email_fields(data)
195
+ extract_email_from_hash(data)
196
+ end
197
+
198
+ # Validate required email fields
199
+ #
200
+ # @param data [Hash] email data
201
+ # @raise [ValidationError] if required fields are missing
202
+ def validate_required_email_fields(data)
203
+ raise ValidationError.missing_field("from") unless data[:from] || data["from"]
204
+ raise ValidationError.missing_field("to") unless data[:to] || data["to"]
205
+ raise ValidationError.missing_field("subject") unless data[:subject] || data["subject"]
206
+ end
207
+
208
+ # Extract email object from hash data
209
+ #
210
+ # @param data [Hash] email data
211
+ # @return [Email] the email object
212
+ def extract_email_from_hash(data)
213
+ Email.new(
214
+ from: data[:from] || data["from"],
215
+ to: data[:to] || data["to"],
216
+ subject: data[:subject] || data["subject"],
217
+ html: data[:html] || data["html"],
218
+ text: data[:text] || data["text"]
219
+ )
220
+ end
221
+ end
222
+ end
@@ -0,0 +1,145 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "uri"
4
+
5
+ module Poodle
6
+ # Configuration class for Poodle SDK settings
7
+ #
8
+ # @example Basic configuration
9
+ # config = Poodle::Configuration.new(api_key: "your_api_key")
10
+ #
11
+ # @example Using environment variables
12
+ # ENV["POODLE_API_KEY"] = "your_api_key"
13
+ # config = Poodle::Configuration.new
14
+ #
15
+ # @example Full configuration
16
+ # config = Poodle::Configuration.new(
17
+ # api_key: "your_api_key",
18
+ # base_url: "https://api.usepoodle.com",
19
+ # timeout: 30,
20
+ # connect_timeout: 10,
21
+ # debug: true
22
+ # )
23
+ class Configuration
24
+ # Default API base URL
25
+ DEFAULT_BASE_URL = "https://api.usepoodle.com"
26
+
27
+ # Default timeout in seconds
28
+ DEFAULT_TIMEOUT = 30
29
+
30
+ # Default connect timeout in seconds
31
+ DEFAULT_CONNECT_TIMEOUT = 10
32
+
33
+ # Maximum content size in bytes (10MB)
34
+ MAX_CONTENT_SIZE = 10 * 1024 * 1024
35
+
36
+ # SDK version
37
+ SDK_VERSION = Poodle::VERSION
38
+
39
+ # @return [String] the API key for authentication
40
+ attr_reader :api_key
41
+
42
+ # @return [String] the base URL for the API
43
+ attr_reader :base_url
44
+
45
+ # @return [Integer] the request timeout in seconds
46
+ attr_reader :timeout
47
+
48
+ # @return [Integer] the connection timeout in seconds
49
+ attr_reader :connect_timeout
50
+
51
+ # @return [Boolean] whether debug mode is enabled
52
+ attr_reader :debug
53
+
54
+ # @return [Hash] additional HTTP client options
55
+ attr_reader :http_options
56
+
57
+ # Initialize a new Configuration
58
+ #
59
+ # @param api_key [String, nil] API key (defaults to POODLE_API_KEY env var)
60
+ # @param base_url [String, nil] Base URL (defaults to POODLE_BASE_URL env var or DEFAULT_BASE_URL)
61
+ # @param timeout [Integer, nil] Request timeout (defaults to POODLE_TIMEOUT env var or DEFAULT_TIMEOUT)
62
+ # @param connect_timeout [Integer, nil] Connect timeout (defaults to POODLE_CONNECT_TIMEOUT env var or
63
+ # DEFAULT_CONNECT_TIMEOUT)
64
+ # @param debug [Boolean] Enable debug mode (defaults to POODLE_DEBUG env var or false)
65
+ # @param http_options [Hash] Additional HTTP client options
66
+ #
67
+ # @raise [ArgumentError] if api_key is missing or invalid
68
+ # @raise [ArgumentError] if base_url is invalid
69
+ # @raise [ArgumentError] if timeout values are invalid
70
+ def initialize(**options)
71
+ api_key = options[:api_key]
72
+ base_url = options[:base_url]
73
+ timeout = options[:timeout]
74
+ connect_timeout = options[:connect_timeout]
75
+ debug = options.fetch(:debug, false)
76
+ http_options = options.fetch(:http_options, {})
77
+ @api_key = api_key || ENV.fetch("POODLE_API_KEY", nil)
78
+ @base_url = base_url || ENV["POODLE_BASE_URL"] || DEFAULT_BASE_URL
79
+ @timeout = timeout || ENV.fetch("POODLE_TIMEOUT", DEFAULT_TIMEOUT).to_i
80
+ @connect_timeout = connect_timeout || ENV.fetch("POODLE_CONNECT_TIMEOUT", DEFAULT_CONNECT_TIMEOUT).to_i
81
+ @debug = debug || ENV["POODLE_DEBUG"] == "true"
82
+ @http_options = http_options
83
+
84
+ validate!
85
+ end
86
+
87
+ # Get the User-Agent string for HTTP requests
88
+ #
89
+ # @return [String] the User-Agent string
90
+ def user_agent
91
+ "poodle-ruby/#{SDK_VERSION} (Ruby #{RUBY_VERSION})"
92
+ end
93
+
94
+ # Get the full URL for an endpoint
95
+ #
96
+ # @param endpoint [String] the API endpoint
97
+ # @return [String] the full URL
98
+ def url_for(endpoint)
99
+ "#{@base_url}/#{endpoint.gsub(%r{^/}, '')}"
100
+ end
101
+
102
+ # Check if debug mode is enabled
103
+ #
104
+ # @return [Boolean] true if debug mode is enabled
105
+ def debug?
106
+ @debug
107
+ end
108
+
109
+ private
110
+
111
+ # Validate the configuration
112
+ #
113
+ # @raise [ArgumentError] if any configuration is invalid
114
+ def validate!
115
+ raise ArgumentError, "API key is required" if @api_key.nil? || @api_key.empty?
116
+
117
+ validate_url!(@base_url, "base_url")
118
+ validate_timeout!(@timeout, "timeout")
119
+ validate_timeout!(@connect_timeout, "connect_timeout")
120
+ end
121
+
122
+ # Validate a URL
123
+ #
124
+ # @param url [String] the URL to validate
125
+ # @param field [String] the field name for error messages
126
+ # @raise [ArgumentError] if the URL is invalid
127
+ def validate_url!(url, field)
128
+ return if url.nil? || url.empty?
129
+
130
+ uri = URI.parse(url)
131
+ raise ArgumentError, "#{field} must be a valid HTTP or HTTPS URL" unless %w[http https].include?(uri.scheme)
132
+ rescue URI::InvalidURIError
133
+ raise ArgumentError, "#{field} must be a valid URL"
134
+ end
135
+
136
+ # Validate a timeout value
137
+ #
138
+ # @param value [Integer] the timeout value
139
+ # @param field [String] the field name for error messages
140
+ # @raise [ArgumentError] if the timeout is invalid
141
+ def validate_timeout!(value, field)
142
+ raise ArgumentError, "#{field} must be a positive integer" unless value.is_a?(Integer) && value.positive?
143
+ end
144
+ end
145
+ end
@@ -0,0 +1,190 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "uri"
4
+ require_relative "configuration"
5
+ require_relative "errors/validation_error"
6
+
7
+ module Poodle
8
+ # Email model representing an email to be sent
9
+ #
10
+ # @example Creating an email
11
+ # email = Poodle::Email.new(
12
+ # from: "sender@example.com",
13
+ # to: "recipient@example.com",
14
+ # subject: "Hello World",
15
+ # html: "<h1>Hello!</h1>",
16
+ # text: "Hello!"
17
+ # )
18
+ #
19
+ # @example HTML only email
20
+ # email = Poodle::Email.new(
21
+ # from: "sender@example.com",
22
+ # to: "recipient@example.com",
23
+ # subject: "Newsletter",
24
+ # html: "<h1>Newsletter</h1><p>Content here</p>"
25
+ # )
26
+ #
27
+ # @example Text only email
28
+ # email = Poodle::Email.new(
29
+ # from: "sender@example.com",
30
+ # to: "recipient@example.com",
31
+ # subject: "Simple notification",
32
+ # text: "This is a simple text notification."
33
+ # )
34
+ class Email
35
+ # @return [String] sender email address
36
+ attr_reader :from
37
+
38
+ # @return [String] recipient email address
39
+ attr_reader :to
40
+
41
+ # @return [String] email subject
42
+ attr_reader :subject
43
+
44
+ # @return [String, nil] HTML content
45
+ attr_reader :html
46
+
47
+ # @return [String, nil] plain text content
48
+ attr_reader :text
49
+
50
+ # Initialize a new Email
51
+ #
52
+ # @param from [String] sender email address
53
+ # @param to [String] recipient email address
54
+ # @param subject [String] email subject
55
+ # @param html [String, nil] HTML content
56
+ # @param text [String, nil] plain text content
57
+ #
58
+ # @raise [ValidationError] if any field is invalid
59
+ def initialize(from:, to:, subject:, html: nil, text: nil)
60
+ @from = from
61
+ @to = to
62
+ @subject = subject
63
+ @html = html
64
+ @text = text
65
+
66
+ validate!
67
+ freeze # Make the object immutable
68
+ end
69
+
70
+ # Convert email to hash for API request
71
+ #
72
+ # @return [Hash] email data as hash
73
+ def to_h
74
+ data = {
75
+ from: @from,
76
+ to: @to,
77
+ subject: @subject
78
+ }
79
+
80
+ data[:html] = @html if @html
81
+ data[:text] = @text if @text
82
+
83
+ data
84
+ end
85
+
86
+ # Convert email to JSON string
87
+ #
88
+ # @return [String] email data as JSON
89
+ def to_json(*args)
90
+ require "json"
91
+ to_h.to_json(*args)
92
+ end
93
+
94
+ # Check if email has HTML content
95
+ #
96
+ # @return [Boolean] true if HTML content is present
97
+ def html?
98
+ !@html.nil? && !@html.empty?
99
+ end
100
+
101
+ # Check if email has text content
102
+ #
103
+ # @return [Boolean] true if text content is present
104
+ def text?
105
+ !@text.nil? && !@text.empty?
106
+ end
107
+
108
+ # Check if email is multipart (has both HTML and text)
109
+ #
110
+ # @return [Boolean] true if both HTML and text content are present
111
+ def multipart?
112
+ html? && text?
113
+ end
114
+
115
+ # Get the size of the email content in bytes
116
+ #
117
+ # @return [Integer] total size of HTML and text content
118
+ def content_size
119
+ size = 0
120
+ size += @html.bytesize if @html
121
+ size += @text.bytesize if @text
122
+ size
123
+ end
124
+
125
+ private
126
+
127
+ # Validate email data
128
+ #
129
+ # @raise [ValidationError] if any validation fails
130
+ def validate!
131
+ validate_required_fields!
132
+ validate_email_addresses!
133
+ validate_content!
134
+ validate_content_size!
135
+ end
136
+
137
+ # Validate required fields
138
+ #
139
+ # @raise [ValidationError] if any required field is missing
140
+ def validate_required_fields!
141
+ raise ValidationError.missing_field("from") if @from.nil? || @from.empty?
142
+ raise ValidationError.missing_field("to") if @to.nil? || @to.empty?
143
+ raise ValidationError.missing_field("subject") if @subject.nil? || @subject.empty?
144
+ end
145
+
146
+ # Validate email addresses
147
+ #
148
+ # @raise [ValidationError] if any email address is invalid
149
+ def validate_email_addresses!
150
+ raise ValidationError.invalid_email(@from, field: "from") unless valid_email?(@from)
151
+ raise ValidationError.invalid_email(@to, field: "to") unless valid_email?(@to)
152
+ end
153
+
154
+ # Validate content presence
155
+ #
156
+ # @raise [ValidationError] if no content is provided
157
+ def validate_content!
158
+ return if html? || text?
159
+
160
+ raise ValidationError.invalid_content
161
+ end
162
+
163
+ # Validate content size
164
+ #
165
+ # @raise [ValidationError] if content is too large
166
+ def validate_content_size!
167
+ max_size = Configuration::MAX_CONTENT_SIZE
168
+
169
+ raise ValidationError.content_too_large("html", max_size) if @html && @html.bytesize > max_size
170
+
171
+ return unless @text && @text.bytesize > max_size
172
+
173
+ raise ValidationError.content_too_large("text", max_size)
174
+ end
175
+
176
+ # Check if an email address is valid
177
+ #
178
+ # @param email [String] the email address to validate
179
+ # @return [Boolean] true if the email is valid
180
+ def valid_email?(email)
181
+ return false if email.nil? || email.empty?
182
+
183
+ # Basic email validation using URI::MailTo
184
+ uri = URI::MailTo.build([email, nil])
185
+ uri.to_s == "mailto:#{email}"
186
+ rescue URI::InvalidComponentError, ArgumentError
187
+ false
188
+ end
189
+ end
190
+ end