pushizo 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: 2f33d90710be708d8c1564539e767f76f570f49505122780696d768ad402b66b
4
+ data.tar.gz: 782a0f46dd24bb0248fa564180316575c2173f5b74115459a3ef494a5e249cd7
5
+ SHA512:
6
+ metadata.gz: 1c674747a8637ae7cae4303f0b2125acda5317649b96004a890d02755160f77610852fd1ea481732e511746d5e60af579b30f0038d384d6e733f8f346cd3c066
7
+ data.tar.gz: 2fc0239d42ebc22d32a228ac235ec33b3d145ee88615844c2b33ae2ad5635ef42f64136884e9fef849b076a787deb546cbe5d417bb0d10660a19d03df11dd19a
data/README.md ADDED
@@ -0,0 +1,170 @@
1
+ <!-- pushizo:header -->
2
+ <p align="center">
3
+ <a href="https://pushizo.com"><img src="https://raw.githubusercontent.com/pushizo/.github/main/profile/assets/mark.svg" alt="Pushizo" width="56" height="56"></a>
4
+ </p>
5
+ <p align="center">
6
+ <a href="https://docs.pushizo.com/docs/sdk/ruby"><img src="https://img.shields.io/badge/docs-docs.pushizo.com-5b4bdb" alt="Docs"></a>
7
+ <a href="https://docs.pushizo.com/docs/api"><img src="https://img.shields.io/badge/API-reference-b44fc4" alt="API reference"></a>
8
+ <a href="https://github.com/pushizo/pushizo-ruby/blob/main/LICENSE"><img src="https://img.shields.io/badge/license-MIT-16130f" alt="License: MIT"></a>
9
+ </p>
10
+ <p align="center"><a href="#english">English</a> · <a href="#türkçe">Türkçe</a></p>
11
+
12
+ ## English
13
+
14
+ **Pushizo Ruby SDK** — Server-side SDK for Ruby. Models and resources are generated from the Pushizo OpenAPI schema; retries, idempotency, pagination and webhook verification are built in. Works with Rails.
15
+
16
+ > **Status:** Publishing to RubyGems is in progress.<br>
17
+ > **Durum:** RubyGems yayını hazırlanıyor.
18
+
19
+ **Install from GitHub until then · O zamana kadar GitHub'dan kurulum:**
20
+
21
+ ```ruby
22
+ # Gemfile
23
+ gem "pushizo", git: "https://github.com/pushizo/pushizo-ruby", tag: "v0.1.0"
24
+ ```
25
+
26
+ **After publication · Yayından sonra:**
27
+
28
+ ```bash
29
+ gem install pushizo
30
+ ```
31
+
32
+ The detailed guide below is in Turkish; code samples are language-neutral. Full documentation:
33
+ [docs.pushizo.com/docs/sdk/ruby](https://docs.pushizo.com/docs/sdk/ruby) · API reference: [docs.pushizo.com/docs/api](https://docs.pushizo.com/docs/api).
34
+
35
+ ## Türkçe
36
+ <!-- /pushizo:header -->
37
+
38
+ # Pushizo Ruby SDK
39
+
40
+ Pushizo public API'si için Ruby sunucu SDK'sı (Ruby 3.0+, `Net::HTTP`; çalışma zamanı bağımlılığı yok).
41
+ Modeller ve kaynak metotları [Pushizo OpenAPI şemasından](https://github.com/pushizo/pushizo-openapi) üretilir (`lib/pushizo/generated/`); kimlik doğrulama,
42
+ yeniden deneme, idempotency, sayfalama ve webhook imza doğrulaması el yazımıdır.
43
+
44
+ ## Kurulum
45
+
46
+ ```ruby
47
+ # Gemfile
48
+ gem "pushizo"
49
+ ```
50
+
51
+ ## Kullanım
52
+
53
+ ```ruby
54
+ require "pushizo"
55
+
56
+ hs = Pushizo::Client.new(api_key: ENV.fetch("HS_API_KEY"), app_id: "app_…") # yerelde base_url: "http://localhost:4000"
57
+
58
+ # Bildirim gönder: Idempotency-Key verilmezse otomatik UUID (yeniden denemelerde aynı anahtar)
59
+ res = hs.notifications.create(
60
+ {
61
+ target: { include_aliases: { external_id: ["user-42"] } },
62
+ contents: { en: "Your order shipped", tr: "Siparişiniz kargoya verildi" }
63
+ },
64
+ idempotency_key: "order-1001"
65
+ )
66
+ puts res.id, res.status
67
+
68
+ # Kullanıcı oluştur/güncelle (Hash veya model nesnesi kabul edilir)
69
+ hs.users.upsert(Pushizo::Models::UpsertUserDto.new(external_id: "user-42", tags: { plan: "pro" }))
70
+
71
+ # Cursor sayfalama: Enumerator, sayfalar tembel yüklenir
72
+ hs.segments.list_all(limit: 100).each { |segment| puts segment.name }
73
+
74
+ # Başka bir app için tek çağrı
75
+ hs.apps.get(app_id: "app_diger")
76
+
77
+ # SDK identity verification
78
+ identity_hash = Pushizo.identity_hash(ENV.fetch("HS_SDK_SECRET"), "user-42")
79
+ ```
80
+
81
+ Yanıtlar `Pushizo::Models::*` nesneleridir: alanlara snake_case metotla (`msg.status`) veya `msg["status"]` ile
82
+ erişilir, `to_h` JSON adlarıyla ham hash döner, bilinmeyen alanlar korunur. Ruby'nin `Object#send` yöntemiyle
83
+ çakışmaması için `events.send` → `send_events`, `campaigns.send` → `send_campaign` olarak adlandırılmıştır.
84
+ SDK'da metodu olmayan uçlar veya JSON dışı yanıtlar için
85
+ `hs.request(:get, "/v1/apps/{appId}/analytics/messages", query: { format: "csv" })` kullanılabilir.
86
+
87
+ ### Ayarlar
88
+
89
+ | Parametre | Varsayılan | Açıklama |
90
+ |-----------|------------|----------|
91
+ | `api_key:` | — | `hs_live_…` API anahtarı (Bearer) |
92
+ | `app_id:` | `nil` | Çağrıda `app_id:` verilmezse kullanılan app |
93
+ | `base_url:` | `https://api.pushizo.com` | API adresi |
94
+ | `timeout:` | `30` | Deneme başına zaman aşımı (sn) |
95
+ | `max_retries:` | `3` | En fazla yeniden deneme (politika: aşağıda) |
96
+ | `auto_idempotency_key:` | `true` | `Idempotency-Key` destekleyen çağrılarda otomatik UUID |
97
+ | `transport:` | `NetHttpTransport` | Özel taşıma (`#call(method, url, headers, body, timeout)`) |
98
+
99
+ Her çağrı `request_options: { timeout: 5, max_retries: 0, headers: { … } }` ile ayrıca ayarlanabilir.
100
+
101
+ ### Hatalar
102
+
103
+ 429 her metotta; 5xx ve ağ hataları GET/HEAD/OPTIONS/PUT/DELETE'te, POST/PATCH'te yalnızca `Idempotency-Key` varsa (otomatik anahtar dahil) veya istek sunucuya hiç gönderilemediyse (bağlantı reddi, DNS hatası, bağlantı zaman aşımı) üstel geri çekilmeyle (0,3 / 1 / 3 sn) yeniden denenir; `Retry-After` başlığına uyulur.
104
+ Kalıcı hatalar `Pushizo::Error` alt sınıflarıdır ve `status`, `code` (`HS_…`), `request_id`, `details` taşır:
105
+
106
+ ```ruby
107
+ begin
108
+ hs.notifications.get("msg_yok")
109
+ rescue Pushizo::NotFoundError
110
+ # 404
111
+ rescue Pushizo::InvalidRequestError => e
112
+ # 400/422 — e.code == Pushizo::ErrorCode::NO_RECIPIENTS
113
+ rescue Pushizo::Error => e
114
+ # AuthenticationError, PermissionDeniedError, ConflictError, RateLimitError, ServerError, NetworkError
115
+ warn "#{e.status} #{e.code} #{e.message}"
116
+ end
117
+ ```
118
+
119
+ ## Webhook imza doğrulaması
120
+
121
+ ```ruby
122
+ # Rails
123
+ class PushizoWebhooksController < ActionController::API
124
+ def create
125
+ events = Pushizo::Webhook.construct_events(
126
+ request.raw_post, # ham gövde
127
+ request.headers["Pushizo-Signature"],
128
+ ENV.fetch("HS_WEBHOOK_SECRET") # rotasyonda [eski, yeni] verilebilir
129
+ )
130
+ events.each { |event| } # tekil veya batch; event["id"] ile tekrarları ayıklayın
131
+ head :no_content
132
+ rescue Pushizo::WebhookSignatureError => e
133
+ render plain: e.reason, status: :bad_request
134
+ end
135
+ end
136
+ ```
137
+
138
+ Zaman damgası 5 dakikadan eskiyse (`tolerance:`) istek tekrar (replay) sayılır. Yalnızca doğrulama için
139
+ `Pushizo::Webhook.verify_signature(...)` `ok?`, `timestamp`, `reason` döner.
140
+
141
+ ## Geliştirme
142
+
143
+ ```bash
144
+ rake test # veya: ruby -Ilib -Itest -e 'Dir["test/test_*.rb"].each { |f| require_relative f }'
145
+ node sdk/scripts/generate-server-sdks.mjs --lang ruby # depo kökünde; generated/ yenilenir
146
+ ```
147
+
148
+ `lib/pushizo/generated/` elle düzenlenmez. Webhook testleri `sdk/testdata/webhook-signature-vectors.json`
149
+ ortak vektörlerini kullanır.
150
+
151
+ <!-- pushizo:footer -->
152
+ ---
153
+
154
+ ## Support · Destek
155
+
156
+ - **Guide · Rehber:** [docs.pushizo.com/docs/sdk/ruby](https://docs.pushizo.com/docs/sdk/ruby)
157
+ - **API reference & OpenAPI · API referansı ve OpenAPI:** [docs.pushizo.com/docs/api](https://docs.pushizo.com/docs/api) ·
158
+ [pushizo-openapi](https://github.com/pushizo/pushizo-openapi)
159
+ - **Bugs & ideas · Hata ve öneri:** [Issues](https://github.com/pushizo/pushizo-ruby/issues)
160
+ - **Security · Güvenlik:** please do not open public issues; email [security@pushizo.com](mailto:security@pushizo.com) ·
161
+ herkese açık issue açmayın, bu adrese yazın.
162
+ - **Help · Destek:** [support@pushizo.com](mailto:support@pushizo.com)
163
+
164
+ This repository is mirrored automatically from the Pushizo main repository on every release. Please open an issue
165
+ instead of a pull request; fixes land in the main repository and ship with the next release.
166
+ Bu depo her sürümde Pushizo ana deposundan otomatik yansıtılır. Pull request yerine issue açın; düzeltmeler ana depoda
167
+ yapılır ve bir sonraki sürümle buraya gelir.
168
+
169
+ Released under the MIT License · MIT lisanslıdır — [LICENSE](https://github.com/pushizo/pushizo-ruby/blob/main/LICENSE).
170
+ <!-- /pushizo:footer -->
@@ -0,0 +1,14 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Pushizo
4
+ # Üretilmiş kaynak sınıflarının tabanı.
5
+ class BaseResource
6
+ def initialize(client)
7
+ @client = client
8
+ end
9
+
10
+ def inspect
11
+ "#<#{self.class.name}>"
12
+ end
13
+ end
14
+ end
@@ -0,0 +1,264 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "erb"
4
+ require "json"
5
+ require "openssl"
6
+ require "securerandom"
7
+ require "time"
8
+
9
+ module Pushizo
10
+ # Pushizo API istemcisi. Kaynak erişimcileri (notifications, users, segments …) api/openapi.yaml'dan üretilir.
11
+ #
12
+ # hs = Pushizo::Client.new(api_key: ENV.fetch("HS_API_KEY"), app_id: "app_…")
13
+ # hs.segments.list_all(limit: 50).each { |segment| puts segment.name }
14
+ class Client
15
+ include GeneratedResources
16
+
17
+ DEFAULT_BASE_URL = "https://api.pushizo.com"
18
+ BACKOFF = [0.3, 1.0, 3.0].freeze
19
+ NETWORK_ERRORS = [
20
+ IOError, EOFError, SocketError, SystemCallError, Timeout::Error, OpenSSL::SSL::SSLError,
21
+ Net::OpenTimeout, Net::ReadTimeout, Net::HTTPBadResponse
22
+ ].freeze
23
+
24
+ # Sunucuda tekrarlanması ek yan etki doğurmayan HTTP metotları; 5xx ve ağ hatalarında serbestçe yeniden denenir.
25
+ IDEMPOTENT_METHODS = %w[GET HEAD OPTIONS PUT DELETE].freeze
26
+
27
+ # Bağlantı kurulmadan oluşan hatalar (DNS çözümlenemedi, bağlantı reddi, ağ/sunucu erişilemez, bağlantı zaman
28
+ # aşımı): istek sunucuya hiç gönderilmemiştir.
29
+ NOT_SENT_ERRORS = [
30
+ SocketError, Errno::ECONNREFUSED, Errno::EHOSTUNREACH, Errno::ENETUNREACH, Net::OpenTimeout
31
+ ].freeze
32
+
33
+ attr_reader :app_id, :base_url
34
+
35
+ # api_key:: hs_live_… API anahtarı (Bearer).
36
+ # app_id:: Çağrılarda app_id: verilmezse kullanılacak varsayılan app.
37
+ # base_url:: API adresi; yerel geliştirmede http://localhost:4000.
38
+ # timeout:: Deneme başına zaman aşımı (sn).
39
+ # max_retries:: 429/5xx ve ağ hatalarında en fazla yeniden deneme.
40
+ # auto_idempotency_key:: Idempotency-Key destekleyen çağrılarda anahtar verilmezse UUID üret.
41
+ # transport:: Özel taşıma (#call); varsayılan NetHttpTransport.
42
+ # sleeper:: Yeniden deneme beklemesi (testlerde değiştirilebilir).
43
+ def initialize(api_key:, app_id: nil, base_url: DEFAULT_BASE_URL, timeout: 30, max_retries: 3,
44
+ auto_idempotency_key: true, user_agent: nil, transport: nil, sleeper: nil)
45
+ raise ArgumentError, "Pushizo: api_key gerekli." if api_key.nil? || api_key.to_s.empty?
46
+
47
+ @api_key = api_key.to_s
48
+ @app_id = app_id
49
+ @base_url = base_url.to_s.sub(%r{/+\z}, "")
50
+ @timeout = timeout
51
+ @max_retries = max_retries
52
+ @auto_idempotency_key = auto_idempotency_key
53
+ @user_agent = user_agent || "pushizo-ruby/#{VERSION}"
54
+ @transport = transport || NetHttpTransport.new
55
+ @sleeper = sleeper || ->(seconds) { sleep(seconds) }
56
+ end
57
+
58
+ # API anahtarı inspect çıktısında görünmez.
59
+ def inspect
60
+ "#<#{self.class.name} base_url=#{@base_url.inspect} app_id=#{@app_id.inspect}>"
61
+ end
62
+
63
+ # SDK identity verification için identity_hash = HMAC-SHA256(sdk_secret, external_id) (hex).
64
+ def self.identity_hash(sdk_secret, external_id)
65
+ Pushizo.identity_hash(sdk_secret, external_id)
66
+ end
67
+
68
+ # Düşük seviyeli istek (SDK'da metodu olmayan uçlar veya format=csv gibi JSON dışı yanıtlar için).
69
+ # path "/v1/…" biçimindedir; {appId} varsa app kimliğiyle doldurulur. JSON yanıt Hash, diğerleri String döner.
70
+ def request(method, path, query: nil, body: nil, app_id: nil, request_options: nil)
71
+ call(method, path, path_params: {}, query: query, body: body, app_id: app_id, request_options: request_options)
72
+ end
73
+
74
+ # İç API: üretilmiş kaynakların çağırdığı yürütücü.
75
+ def call(method, path, path_params: {}, query: nil, body: nil, form: nil, app_id: nil, query_app_id: false,
76
+ body_app_id: false, idempotent: false, idempotency_key: nil, response: nil, request_options: nil)
77
+ opts = request_options || {}
78
+ url_path = path.dup
79
+ url_path = url_path.gsub("{appId}", escape(resolve_app_id(app_id))) if url_path.include?("{appId}")
80
+ path_params.each do |name, value|
81
+ raise ArgumentError, "Pushizo: '#{name}' yol parametresi boş olamaz." if value.nil? || value.to_s.empty?
82
+
83
+ url_path = url_path.gsub("{#{name}}", escape(value.to_s))
84
+ end
85
+
86
+ items = []
87
+ items << ["app_id", resolve_app_id(app_id)] if query_app_id
88
+ (query || {}).each do |key, value|
89
+ next if value.nil?
90
+
91
+ Array(value.is_a?(Hash) ? [value] : value).each { |v| items << [key.to_s, scalar(v)] unless v.nil? }
92
+ end
93
+ url = "#{@base_url}#{url_path}"
94
+ url += "?#{items.map { |k, v| "#{escape(k)}=#{escape(v)}" }.join('&')}" unless items.empty?
95
+
96
+ key = idempotency_key || opts[:idempotency_key]
97
+ headers = {
98
+ "Authorization" => "Bearer #{@api_key}",
99
+ "Accept" => "application/json",
100
+ "User-Agent" => @user_agent
101
+ }
102
+ payload = nil
103
+ if form
104
+ boundary = "----pushizo#{SecureRandom.hex(16)}"
105
+ payload = multipart(form, boundary)
106
+ headers["Content-Type"] = "multipart/form-data; boundary=#{boundary}"
107
+ elsif !body.nil?
108
+ data = Types.serialize(body)
109
+ if data.is_a?(Hash)
110
+ data = data.merge("app_id" => resolve_app_id(app_id)) if body_app_id && data["app_id"].to_s.empty?
111
+ key ||= data["idempotency_key"] if idempotent && data["idempotency_key"].is_a?(String)
112
+ end
113
+ payload = JSON.generate(data)
114
+ headers["Content-Type"] = "application/json"
115
+ end
116
+ key = SecureRandom.uuid if (key.nil? || key.to_s.empty?) && idempotent && @auto_idempotency_key
117
+ headers["Idempotency-Key"] = key.to_s if key && !key.to_s.empty?
118
+ (opts[:headers] || {}).each { |k, v| headers[k.to_s] = v.to_s }
119
+
120
+ raw = send_with_retries(method, url, headers, payload, opts)
121
+ response ? Types.coerce(raw, response) : raw
122
+ end
123
+
124
+ # İç API: data / next_cursor / has_more zarfını sayfa sayfa dolaşan Enumerator.
125
+ def paginate(cursor)
126
+ Enumerator.new do |yielder|
127
+ next_cursor = cursor
128
+ loop do
129
+ page = yield(next_cursor)
130
+ data = page.is_a?(BaseModel) ? page["data"] : (page || {})["data"]
131
+ (data || []).each { |item| yielder << item }
132
+ next_cursor = page["next_cursor"]
133
+ break unless page["has_more"] && next_cursor && !next_cursor.to_s.empty?
134
+ end
135
+ end
136
+ end
137
+
138
+ private
139
+
140
+ def resolve_app_id(app_id)
141
+ id = app_id || @app_id
142
+ raise ArgumentError, "Pushizo: app_id verilmedi (istemci seçeneği veya çağrı parametresi)." if id.nil? || id.to_s.empty?
143
+
144
+ id.to_s
145
+ end
146
+
147
+ def escape(value)
148
+ ERB::Util.url_encode(value.to_s)
149
+ end
150
+
151
+ def scalar(value)
152
+ case value
153
+ when String then value
154
+ when Symbol then value.to_s
155
+ when true, false, Numeric then value.to_s
156
+ else JSON.generate(Types.serialize(value))
157
+ end
158
+ end
159
+
160
+ def send_with_retries(method, url, headers, payload, opts)
161
+ timeout = opts[:timeout] || @timeout
162
+ max_retries = opts.key?(:max_retries) ? opts[:max_retries] : @max_retries
163
+ has_key = headers.any? { |k, v| k.to_s.casecmp?("idempotency-key") && !v.to_s.empty? }
164
+ attempt = 0
165
+ loop do
166
+ error = nil
167
+ not_sent = false
168
+ begin
169
+ status, response_headers, response_body = @transport.call(method, url, headers, payload, timeout)
170
+ return parse_body(response_headers, response_body) if status.between?(200, 299)
171
+
172
+ error = error_from(status, response_headers, response_body)
173
+ rescue *NETWORK_ERRORS => e
174
+ error = NetworkError.new(0, ErrorCode::NETWORK, e.message.to_s.empty? ? e.class.name : e.message)
175
+ not_sent = NOT_SENT_ERRORS.any? { |klass| e.is_a?(klass) }
176
+ end
177
+ raise error if attempt >= max_retries || !retry?(method, error, has_key, not_sent)
178
+
179
+ delay = error.retry_after && error.retry_after.positive? ? error.retry_after : BACKOFF[[attempt, BACKOFF.size - 1].min]
180
+ @sleeper.call(delay)
181
+ attempt += 1
182
+ end
183
+ end
184
+
185
+ # Yeniden deneme politikası: 429 her metotta (istek işlenmeden reddedilmiştir); 5xx ve ağ hataları idempotent
186
+ # metotlarda, POST/PATCH'te yalnızca Idempotency-Key varsa ya da istek sunucuya hiç gönderilmediyse. Aksi hâlde
187
+ # sunucu isteği işlemiş olabilir ve tekrar çift kayıt üretebilir.
188
+ def retry?(method, error, has_idempotency_key, not_sent)
189
+ return true if error.status == 429
190
+ return false unless error.retryable?
191
+
192
+ IDEMPOTENT_METHODS.include?(method.to_s.upcase) || has_idempotency_key || not_sent
193
+ end
194
+
195
+ def parse_body(headers, body)
196
+ return nil if body.nil? || body.empty?
197
+
198
+ content_type = headers["content-type"].to_s
199
+ return body unless content_type.empty? || content_type.include?("json")
200
+
201
+ JSON.parse(body)
202
+ rescue JSON::ParserError
203
+ body
204
+ end
205
+
206
+ def error_from(status, headers, body)
207
+ envelope = begin
208
+ parsed = JSON.parse(body.to_s)
209
+ parsed.is_a?(Hash) && parsed["error"].is_a?(Hash) ? parsed["error"] : {}
210
+ rescue JSON::ParserError
211
+ {}
212
+ end
213
+ details = envelope["details"].is_a?(Hash) ? envelope["details"] : nil
214
+ Error.class_for(status).new(
215
+ status,
216
+ (envelope["code"] || "HTTP_#{status}").to_s,
217
+ (envelope["message"] || "HTTP #{status}").to_s,
218
+ request_id: envelope["request_id"] || headers["x-request-id"],
219
+ details: details,
220
+ retry_after: parse_retry_after(headers["retry-after"])
221
+ )
222
+ end
223
+
224
+ # Retry-After: saniye veya HTTP tarihi.
225
+ def parse_retry_after(value)
226
+ return nil if value.nil? || value.to_s.strip.empty?
227
+
228
+ text = value.to_s.strip
229
+ if text.match?(/\A\d+(\.\d+)?\z/)
230
+ seconds = text.to_f
231
+ return seconds.positive? ? seconds : nil
232
+ end
233
+ delta = Time.httpdate(text) - Time.now
234
+ delta.positive? ? delta : nil
235
+ rescue ArgumentError
236
+ nil
237
+ end
238
+
239
+ def multipart(form, boundary)
240
+ out = +"".b
241
+ form.each do |name, value|
242
+ next if value.nil?
243
+
244
+ out << "--#{boundary}\r\n".b
245
+ if value.is_a?(FileUpload) || value.respond_to?(:read)
246
+ file = FileUpload.wrap(value)
247
+ out << "Content-Disposition: form-data; name=\"#{quote(name)}\"; filename=\"#{quote(file.filename)}\"\r\n".b
248
+ out << "Content-Type: #{file.content_type}\r\n\r\n".b
249
+ out << file.content
250
+ else
251
+ out << "Content-Disposition: form-data; name=\"#{quote(name)}\"\r\n\r\n".b
252
+ out << scalar(value).b
253
+ end
254
+ out << "\r\n".b
255
+ end
256
+ out << "--#{boundary}--\r\n".b
257
+ out
258
+ end
259
+
260
+ def quote(value)
261
+ value.to_s.gsub("\\", "\\\\\\\\").gsub('"', '\\"').delete("\r\n")
262
+ end
263
+ end
264
+ end
@@ -0,0 +1,96 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Pushizo
4
+ # API hata zarfındaki error.code değerleri (HS_ önekli katalog).
5
+ module ErrorCode
6
+ VALIDATION_ERROR = "HS_VALIDATION_ERROR"
7
+ BAD_REQUEST = "HS_BAD_REQUEST"
8
+ UNAUTHENTICATED = "HS_UNAUTHENTICATED"
9
+ FORBIDDEN = "HS_FORBIDDEN"
10
+ TENANT_MISMATCH = "HS_TENANT_MISMATCH"
11
+ NOT_FOUND = "HS_NOT_FOUND"
12
+ CONFLICT = "HS_CONFLICT"
13
+ IDEMPOTENCY_CONFLICT = "HS_IDEMPOTENCY_CONFLICT"
14
+ IDEMPOTENCY_IN_PROGRESS = "HS_IDEMPOTENCY_IN_PROGRESS"
15
+ NO_RECIPIENTS = "HS_NO_RECIPIENTS"
16
+ CHANNEL_NOT_CONFIGURED = "HS_CHANNEL_NOT_CONFIGURED"
17
+ CONTENT_INVALID = "HS_CONTENT_INVALID"
18
+ SUBSCRIPTION_INVALID = "HS_SUBSCRIPTION_INVALID"
19
+ UNPROCESSABLE = "HS_UNPROCESSABLE"
20
+ RATE_LIMITED = "HS_RATE_LIMITED"
21
+ QUOTA_EXCEEDED = "HS_QUOTA_EXCEEDED"
22
+ INTERNAL = "HS_INTERNAL"
23
+ NOT_IMPLEMENTED = "HS_NOT_IMPLEMENTED"
24
+ PROVIDER_UNAVAILABLE = "HS_PROVIDER_UNAVAILABLE"
25
+ SERVICE_UNAVAILABLE = "HS_SERVICE_UNAVAILABLE"
26
+ # İstemci tarafı: bağlantı kurulamadı veya zaman aşımı (status 0).
27
+ NETWORK = "NETWORK"
28
+ end
29
+
30
+ # API hata zarfından ({"error": {"code", "message", "request_id", "details"}}) türetilen istisna.
31
+ # HTTP durumuna göre alt sınıflar fırlatılır (NotFoundError, RateLimitError …).
32
+ class Error < StandardError
33
+ attr_reader :status, :code, :request_id, :details, :retry_after
34
+
35
+ def initialize(status, code, message, request_id: nil, details: nil, retry_after: nil)
36
+ super(message)
37
+ @status = status
38
+ @code = code
39
+ @request_id = request_id
40
+ @details = details
41
+ @retry_after = retry_after
42
+ end
43
+
44
+ # 429, 5xx veya ağ hatası: yeniden denenebilir.
45
+ def retryable?
46
+ status.zero? || status == 429 || status >= 500
47
+ end
48
+
49
+ def to_s
50
+ suffix = request_id ? " (request_id=#{request_id})" : ""
51
+ "[#{status} #{code}] #{super}#{suffix}"
52
+ end
53
+
54
+ # HTTP durumuna karşılık gelen sınıf.
55
+ def self.class_for(status)
56
+ case status
57
+ when 0 then NetworkError
58
+ when 400, 422 then InvalidRequestError
59
+ when 401 then AuthenticationError
60
+ when 403 then PermissionDeniedError
61
+ when 404 then NotFoundError
62
+ when 409 then ConflictError
63
+ when 429 then RateLimitError
64
+ else status >= 500 ? ServerError : Error
65
+ end
66
+ end
67
+ end
68
+
69
+ # Bağlantı kurulamadı veya zaman aşımı (status 0).
70
+ class NetworkError < Error; end
71
+ # 400 / 422: istek doğrulanamadı veya işlenemedi (details alan hatalarını içerir).
72
+ class InvalidRequestError < Error; end
73
+ # 401: API anahtarı eksik veya geçersiz.
74
+ class AuthenticationError < Error; end
75
+ # 403: anahtarın kapsamı yetersiz, IP izin listesi dışı veya kaynak başka kiracıya ait.
76
+ class PermissionDeniedError < Error; end
77
+ # 404: kaynak bulunamadı.
78
+ class NotFoundError < Error; end
79
+ # 409: kaynak çakışması veya idempotency anahtarı çakışması.
80
+ class ConflictError < Error; end
81
+ # 429: hız sınırı veya plan kotası aşıldı.
82
+ class RateLimitError < Error; end
83
+ # 5xx: sunucu veya sağlayıcı geçici olarak kullanılamıyor.
84
+ class ServerError < Error; end
85
+
86
+ # Webhook imzası geçersiz. reason: missing_header, malformed_header, timestamp_out_of_tolerance,
87
+ # signature_mismatch veya malformed_body.
88
+ class WebhookSignatureError < StandardError
89
+ attr_reader :reason
90
+
91
+ def initialize(reason)
92
+ super("Pushizo webhook imzası geçersiz: #{reason}")
93
+ @reason = reason
94
+ end
95
+ end
96
+ end
@@ -0,0 +1,30 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Pushizo
4
+ # Multipart dosya alanı. İçerik bellekte tutulur ki yeniden denemeler aynı gövdeyi göndersin.
5
+ class FileUpload
6
+ attr_reader :filename, :content, :content_type
7
+
8
+ def initialize(content, filename: "upload", content_type: nil)
9
+ @content = content.to_s.b
10
+ @filename = filename.to_s.empty? ? "upload" : filename.to_s
11
+ @content_type = content_type || (@filename.end_with?(".csv") ? "text/csv" : "application/octet-stream")
12
+ end
13
+
14
+ # Diskteki dosyayı okur.
15
+ def self.from_path(path, content_type: nil)
16
+ new(File.binread(path), filename: File.basename(path), content_type: content_type)
17
+ end
18
+
19
+ # FileUpload, IO (ör. File.open(path, "rb")) veya String içeriği FileUpload'a çevirir.
20
+ def self.wrap(value)
21
+ case value
22
+ when FileUpload then value
23
+ when String then new(value)
24
+ else
25
+ name = value.respond_to?(:path) && value.path ? File.basename(value.path) : "upload"
26
+ new(value.read, filename: name)
27
+ end
28
+ end
29
+ end
30
+ end