hooksniff 1.2.0 → 1.3.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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 441fb04faed964e0da0b8827caa0f3f13bd50ff3eec641a928524cecda99d56f
4
- data.tar.gz: 937d0f461fb8f52c3dec0a69b0849b8d603719ff883c7ec11ed72ca0bb0dac63
3
+ metadata.gz: a947ca7c8c9c75d2aac6b59f30cd1e0c7f6096aee40816fb16bba954ff60570b
4
+ data.tar.gz: da3d3b65337ff775633efc6ce7797cdd23033a3f2b1b9cfb5bdc3096b485f4e1
5
5
  SHA512:
6
- metadata.gz: 6fb0d72135f9c5e271ef0f9cfc2aec85818fdc164f945b80fb64415196f9b6142408559ea9c3c19a3d1ec6d152f2eaaf691d99446dbb8b7b9b9f38961931b63a
7
- data.tar.gz: 3aa6ffb82540a1352671673422fc7751bf47037d262b3e515cbcd5579bc4d027628ef0143834d8c01ad12f8052175391ba5e7591ff8ae38f5577884c7dd96540
6
+ metadata.gz: f609bcef3b51f1fee6540b705bb9191f54b7cc3c681346a08b1482d1aeef0a52e661f0b26f6e432f30995bbff1b54ae043c775b42938c25975da4fcfe36ac3f8
7
+ data.tar.gz: a3c6d7bf7516cf4e41fdb531af663d8036056210e853a54c46674e2a56f60a41523b7f53ac06bd4c1e226711c0143281d8f276a89e39820447757b1b2275998a
data/Gemfile.lock CHANGED
@@ -1,7 +1,7 @@
1
1
  PATH
2
2
  remote: .
3
3
  specs:
4
- svix (1.93.0)
4
+ hooksniff (1.2.0)
5
5
  base64 (~> 0.3.0)
6
6
  logger (~> 1.0)
7
7
 
@@ -45,11 +45,11 @@ PLATFORMS
45
45
 
46
46
  DEPENDENCIES
47
47
  base64
48
+ hooksniff!
48
49
  logger
49
50
  ostruct
50
51
  rake (~> 13.0)
51
52
  rspec (~> 3.2)
52
- svix!
53
53
  webmock (~> 3.25)
54
54
 
55
55
  BUNDLED WITH
data/README.md CHANGED
@@ -1,14 +1,18 @@
1
1
  # HookSniff Ruby SDK
2
2
 
3
3
  <p align="center">
4
- <a href="https://github.com/servetarslan02/HookSniff"><img src="https://img.shields.io/github/license/servetarslan02/HookSniff" alt="License"></a>
5
- <a href="https://rubygems.org/gems/hooksniff"><img src="https://img.shields.io/gems/v/hooksniff" alt="Gem"></a>
4
+ <a href="https://rubygems.org/gems/hooksniff"><img src="https://img.shields.io/gem/v/hooksniff.svg" alt="Gem"></a>
5
+ <a href="https://github.com/servetarslan02/hooksniff-ruby"><img src="https://img.shields.io/github/license/servetarslan02/hooksniff-ruby" alt="License"></a>
6
6
  </p>
7
7
 
8
- Ruby SDK for the [HookSniff](https://hooksniff.com) webhook delivery platform.
8
+ Ruby SDK for the [HookSniff](https://hooksniff.vercel.app) webhook delivery platform.
9
9
 
10
10
  ## Installation
11
11
 
12
+ ```ruby
13
+ gem 'hooksniff'
14
+ ```
15
+
12
16
  ```bash
13
17
  gem install hooksniff
14
18
  ```
@@ -16,61 +20,53 @@ gem install hooksniff
16
20
  ## Quick Start
17
21
 
18
22
  ```ruby
19
- require "hooksniff"
23
+ require 'hooksniff'
20
24
 
21
- client = HookSniff::Client.new("hs_xxx")
25
+ hs = HookSniff::Client.new('hooksniff_xxx')
22
26
 
23
27
  # List endpoints
24
- endpoints = client.endpoint.list
28
+ endpoints = hs.endpoints.list
25
29
 
26
- # Create a message
27
- msg = client.message.create(event_type: "order.created", payload: { order_id: "123" })
30
+ # Send a webhook
31
+ delivery = hs.messages.create(
32
+ endpoint_id: 'ep_xxx',
33
+ event: 'order.created',
34
+ data: { order_id: '123', amount: 99.99 }
35
+ )
36
+ ```
37
+
38
+ ## Webhook Verification
28
39
 
29
- # Verify webhook signature
30
- wh = HookSniff::Webhook.new("whsec_xxx")
40
+ ```ruby
41
+ wh = HookSniff::Webhook.new('whsec_xxx')
31
42
  payload = wh.verify(body, headers)
32
43
  ```
33
44
 
34
- ## Resources (30+)
35
-
36
- | Resource | Description |
37
- |----------|-------------|
38
- | **Endpoint** | CRUD, secret rotation, headers |
39
- | **Message** | Create, list, get |
40
- | **MessageAttempt** | List by endpoint/msg, get, resend |
41
- | **Authentication** | Logout |
42
- | **EventType** | CRUD |
43
- | **Statistics** | Aggregate stats |
44
- | **Health** | API health check |
45
- | **Environment** | Environment & variable management (Faz 8) |
46
- | **BackgroundTask** | List, get, cancel (Faz 9) |
47
- | **OperationalWebhook** | Endpoint & delivery management (Faz 10) |
48
- | **MessagePoller** | Poll, seek, commit (Faz 11) |
49
- | **Inbound** | Inbound webhook configs (Faz 12) |
50
- | **Connector** | Connector & config management (Faz 13) |
51
- | **Integration** | CRUD, test, events, stats (Faz 14) |
52
- | **Stream** | Channels, subscriptions, publish (Faz 15) |
53
- | **Application** | Application management |
54
- | **ApiKey** | API key CRUD, rotate |
55
- | **Search** | Full-text delivery search |
56
- | **Alert** | Alert rule CRUD, test |
57
- | **Analytics** | Delivery trends, success rate, latency |
58
- | **Billing** | Subscription, usage, invoices, portal |
59
- | **Portal** | Profile, plan, notifications |
60
- | **Team** | Teams, invites, members, roles |
61
- | **Notification** | List, read, unread count |
62
- | **Sso** | SSO config management |
63
- | **AuditLog** | Audit entry listing |
64
- | **CustomDomain** | Domain management, verification |
65
- | **RateLimit** | Per-endpoint rate limits |
66
- | **Routing** | Routing rules, endpoint health |
67
- | **Template** | Template listing, apply |
68
- | **Schema** | Schema registry, validation |
69
- | **Playground** | Test webhooks |
70
- | **ServiceToken** | Service token management |
71
-
72
- ## Links
73
-
74
- - [Documentation](https://docs.hooksniff.com)
75
- - [API Reference](https://api.hooksniff.com)
76
- - [GitHub](https://github.com/servetarslan02/HookSniff)
45
+ ## API Resources
46
+
47
+ | Resource | Methods |
48
+ |----------|---------|
49
+ | Endpoints | list, create, get, update, delete, rotate_secret |
50
+ | Messages | create, list, get |
51
+ | MessageAttempts | list, get, resend, list_by_msg |
52
+ | EventTypes | list, create, get, update, delete |
53
+ | Stream | list_channels, get_channel, create_channel, subscribe, publish |
54
+ | Authentication | login, register, logout |
55
+ | BackgroundTasks | list, get |
56
+ | Connectors | list, get |
57
+ | Integrations | list, get, create, update, delete |
58
+ | Inbound | list, create, get, delete |
59
+
60
+ ## Features
61
+
62
+ - HMAC-SHA256 webhook verification
63
+ - Typed webhook events
64
+ - Automatic retry with exponential backoff
65
+ - Pagination helpers
66
+ - Rate limit header parsing
67
+ - SSE streaming
68
+ - Idempotency keys
69
+
70
+ ## License
71
+
72
+ MIT
data/hooksniff.gemspec CHANGED
@@ -8,7 +8,7 @@ Gem::Specification.new do |spec|
8
8
  spec.name = "hooksniff"
9
9
  spec.version = HookSniff::VERSION
10
10
  spec.authors = ["HookSniff"]
11
- spec.email = ["support@hooksniff.vercel.app"]
11
+ spec.email = ["servetarslan02@gmail.com"]
12
12
  spec.license = "MIT"
13
13
 
14
14
  spec.summary = "HookSniff webhooks API client and webhook verification library"
@@ -8,19 +8,6 @@ module HookSniff
8
8
  @client = client
9
9
  end
10
10
 
11
- def app_portal_access(app_id, app_portal_access_in, options = {})
12
- options = options.transform_keys(&:to_s)
13
- res = @client.execute_request(
14
- "POST",
15
- "/v1/auth/portal-access/#{app_id}",
16
- headers: {
17
- "idempotency-key" => options["idempotency-key"]
18
- },
19
- body: app_portal_access_in
20
- )
21
- DashboardAccessOut.deserialize(res)
22
- end
23
-
24
11
  def logout(options = {})
25
12
  options = options.transform_keys(&:to_s)
26
13
  @client.execute_request(
@@ -7,36 +7,36 @@ module HookSniff
7
7
  end
8
8
 
9
9
  def list
10
- @client.request(:get, "/api/v1/integrations")
10
+ @client.request(:get, "/v1/integrations")
11
11
  end
12
12
 
13
13
  def get(id)
14
- @client.request(:get, "/api/v1/integrations/#{id}")
14
+ @client.request(:get, "/v1/integrations/#{id}")
15
15
  end
16
16
 
17
17
  create_attrs = %i[name description connector_config_id endpoint_id event_filter transform_id retry_policy metadata enabled]
18
18
  def create(attrs)
19
- @client.request(:post, "/api/v1/integrations", attrs)
19
+ @client.request(:post, "/v1/integrations", attrs)
20
20
  end
21
21
 
22
22
  def update(id, attrs)
23
- @client.request(:put, "/api/v1/integrations/#{id}", attrs)
23
+ @client.request(:put, "/v1/integrations/#{id}", attrs)
24
24
  end
25
25
 
26
26
  def delete(id)
27
- @client.request(:delete, "/api/v1/integrations/#{id}")
27
+ @client.request(:delete, "/v1/integrations/#{id}")
28
28
  end
29
29
 
30
30
  def test(id)
31
- @client.request(:post, "/api/v1/integrations/#{id}/test")
31
+ @client.request(:post, "/v1/integrations/#{id}/test")
32
32
  end
33
33
 
34
34
  def list_events(id, params = {})
35
- @client.request(:get, "/api/v1/integrations/#{id}/events", params)
35
+ @client.request(:get, "/v1/integrations/#{id}/events", params)
36
36
  end
37
37
 
38
38
  def get_stats(id)
39
- @client.request(:get, "/api/v1/integrations/#{id}/stats")
39
+ @client.request(:get, "/v1/integrations/#{id}/stats")
40
40
  end
41
41
  end
42
42
  end
@@ -20,18 +20,5 @@ module HookSniff
20
20
  )
21
21
  AggregateEventTypesOut.deserialize(res)
22
22
  end
23
-
24
- def app_stats(app_id, options = {})
25
- options = options.transform_keys(&:to_s)
26
- res = @client.execute_request(
27
- "GET",
28
- "/v1/stats/app/#{app_id}",
29
- query_params: {
30
- "since" => options["since"],
31
- "until" => options["until"]
32
- }
33
- )
34
- res
35
- end
36
23
  end
37
24
  end
@@ -7,39 +7,58 @@ module HookSniff
7
7
  end
8
8
 
9
9
  def list_channels
10
- @client.request(:get, "/api/v1/stream/channels")
10
+ @client.request(:get, "/v1/stream/channels")
11
11
  end
12
12
 
13
13
  def get_channel(id)
14
- @client.request(:get, "/api/v1/stream/channels/#{id}")
14
+ @client.request(:get, "/v1/stream/channels/#{id}")
15
15
  end
16
16
 
17
17
  def create_channel(attrs)
18
- @client.request(:post, "/api/v1/stream/channels", attrs)
18
+ @client.request(:post, "/v1/stream/channels", attrs)
19
19
  end
20
20
 
21
21
  def update_channel(id, attrs)
22
- @client.request(:put, "/api/v1/stream/channels/#{id}", attrs)
22
+ @client.request(:put, "/v1/stream/channels/#{id}", attrs)
23
23
  end
24
24
 
25
25
  def delete_channel(id)
26
- @client.request(:delete, "/api/v1/stream/channels/#{id}")
26
+ @client.request(:delete, "/v1/stream/channels/#{id}")
27
27
  end
28
28
 
29
29
  def list_messages(id, params = {})
30
- @client.request(:get, "/api/v1/stream/channels/#{id}/messages", params)
30
+ @client.request(:get, "/v1/stream/channels/#{id}/messages", params)
31
31
  end
32
32
 
33
33
  def list_subscriptions
34
- @client.request(:get, "/api/v1/stream/subscriptions")
34
+ @client.request(:get, "/v1/stream/subscriptions")
35
35
  end
36
36
 
37
37
  def disconnect_subscription(id)
38
- @client.request(:delete, "/api/v1/stream/subscriptions/#{id}")
38
+ @client.request(:delete, "/v1/stream/subscriptions/#{id}")
39
39
  end
40
40
 
41
41
  def publish(body)
42
- @client.request(:post, "/api/v1/stream/publish", body)
42
+ @client.request(:post, "/v1/stream/publish", body)
43
43
  end
44
44
  end
45
45
  end
46
+
47
+ # Subscribe to real-time events via SSE
48
+ # @param channel_id [String] Channel ID to subscribe to
49
+ # @param block [Block] Callback for each event
50
+ # @return [void]
51
+ def subscribe(channel_id, &block)
52
+ @client.request_stream(:get, "/v1/stream/channels/#{channel_id}/subscribe") do |event|
53
+ block.call(event)
54
+ end
55
+ end
56
+
57
+ # Subscribe to delivery events (legacy SSE endpoint)
58
+ # @param block [Block] Callback for each event
59
+ # @return [void]
60
+ def subscribe_deliveries(&block)
61
+ @client.request_stream(:get, "/v1/stream/deliveries") do |event|
62
+ block.call(event)
63
+ end
64
+ end
@@ -41,6 +41,13 @@ module HookSniff
41
41
  end
42
42
  end
43
43
 
44
+ # 408 Request Timeout
45
+ class RequestTimeoutError < ApiError
46
+ def initialize(response_headers: {}, response_body: nil)
47
+ super(code: 408, response_headers: response_headers, response_body: response_body)
48
+ end
49
+ end
50
+
44
51
  # 409 Conflict
45
52
  class ConflictError < ApiError
46
53
  def initialize(response_headers: {}, response_body: nil)
@@ -48,6 +55,20 @@ module HookSniff
48
55
  end
49
56
  end
50
57
 
58
+ # 410 Gone
59
+ class GoneError < ApiError
60
+ def initialize(response_headers: {}, response_body: nil)
61
+ super(code: 410, response_headers: response_headers, response_body: response_body)
62
+ end
63
+ end
64
+
65
+ # 413 Payload Too Large
66
+ class PayloadTooLargeError < ApiError
67
+ def initialize(response_headers: {}, response_body: nil)
68
+ super(code: 413, response_headers: response_headers, response_body: response_body)
69
+ end
70
+ end
71
+
51
72
  # 422 Unprocessable Entity
52
73
  class UnprocessableEntityError < ApiError
53
74
  attr_reader :validation_errors
@@ -75,6 +96,13 @@ module HookSniff
75
96
  end
76
97
  end
77
98
 
99
+ # 501 Not Implemented
100
+ class NotImplementedError < ApiError
101
+ def initialize(response_headers: {}, response_body: nil)
102
+ super(code: 501, response_headers: response_headers, response_body: response_body)
103
+ end
104
+ end
105
+
78
106
  # 502 Bad Gateway
79
107
  class BadGatewayError < ApiError
80
108
  def initialize(response_headers: {}, response_body: nil)
@@ -96,6 +124,45 @@ module HookSniff
96
124
  end
97
125
  end
98
126
 
127
+ # 507 Insufficient Storage
128
+ class InsufficientStorageError < ApiError
129
+ def initialize(response_headers: {}, response_body: nil)
130
+ super(code: 507, response_headers: response_headers, response_body: response_body)
131
+ end
132
+ end
133
+
134
+ # 508 Loop Detected
135
+ class LoopDetectedError < ApiError
136
+ def initialize(response_headers: {}, response_body: nil)
137
+ super(code: 508, response_headers: response_headers, response_body: response_body)
138
+ end
139
+ end
140
+
141
+ # Timeout — request exceeded the configured timeout
142
+ class TimeoutError < StandardError
143
+ attr_reader :code
144
+ def initialize(message = "Request timeout")
145
+ @code = 0
146
+ super(message)
147
+ end
148
+ end
149
+
150
+ # Network error — connection failed
151
+ class NetworkError < StandardError
152
+ attr_reader :code
153
+ def initialize(message = "Network error")
154
+ @code = 0
155
+ super(message)
156
+ end
157
+ end
158
+
159
+ # Authentication error — token invalid, expired, or missing
160
+ class AuthenticationError < ApiError
161
+ def initialize(response_headers: {}, response_body: nil)
162
+ super(code: 401, response_headers: response_headers, response_body: response_body)
163
+ end
164
+ end
165
+
99
166
  # Create the appropriate error from a status code
100
167
  def self.create_error_from_status(status_code, response_headers: {}, response_body: nil)
101
168
  case status_code
@@ -107,8 +174,14 @@ module HookSniff
107
174
  ForbiddenError.new(response_headers: response_headers, response_body: response_body)
108
175
  when 404
109
176
  NotFoundError.new(response_headers: response_headers, response_body: response_body)
177
+ when 408
178
+ RequestTimeoutError.new(response_headers: response_headers, response_body: response_body)
110
179
  when 409
111
180
  ConflictError.new(response_headers: response_headers, response_body: response_body)
181
+ when 410
182
+ GoneError.new(response_headers: response_headers, response_body: response_body)
183
+ when 413
184
+ PayloadTooLargeError.new(response_headers: response_headers, response_body: response_body)
112
185
  when 422
113
186
  UnprocessableEntityError.new(response_headers: response_headers, response_body: response_body)
114
187
  when 429
@@ -116,12 +189,18 @@ module HookSniff
116
189
  RateLimitError.new(retry_after: retry_after, response_headers: response_headers, response_body: response_body)
117
190
  when 500
118
191
  InternalServerError.new(response_headers: response_headers, response_body: response_body)
192
+ when 501
193
+ NotImplementedError.new(response_headers: response_headers, response_body: response_body)
119
194
  when 502
120
195
  BadGatewayError.new(response_headers: response_headers, response_body: response_body)
121
196
  when 503
122
197
  ServiceUnavailableError.new(response_headers: response_headers, response_body: response_body)
123
198
  when 504
124
199
  GatewayTimeoutError.new(response_headers: response_headers, response_body: response_body)
200
+ when 507
201
+ InsufficientStorageError.new(response_headers: response_headers, response_body: response_body)
202
+ when 508
203
+ LoopDetectedError.new(response_headers: response_headers, response_body: response_body)
125
204
  else
126
205
  ApiError.new(code: status_code, response_headers: response_headers, response_body: response_body)
127
206
  end
@@ -47,7 +47,9 @@ module HookSniff
47
47
  # Create request object
48
48
  request = request_class.new(uri.request_uri)
49
49
  request["Authorization"] = "Bearer #{@token}"
50
- request["User-Agent"] = "hooksniff-libs/#{VERSION}/ruby"
50
+ sdk_ua = "hooksniff-libs/#{VERSION}/ruby"
51
+ request["User-Agent"] = sdk_ua
52
+ request["X-HookSniff-SDK"] = sdk_ua
51
53
  request["hooksniff-req-id"] = rand(0...(2 ** 64))
52
54
 
53
55
  # Add headers
@@ -83,26 +85,41 @@ module HookSniff
83
85
  end
84
86
 
85
87
  private def execute_request_with_retries(request, http)
86
- res = http.request(request)
88
+ res = nil
89
+ retries = [1, 2, 4] # seconds: 1s, 2s, 4s exponential backoff
90
+ max_retries = retries.length
91
+
92
+ retries.each_with_index do |sleep_duration, index|
93
+ begin
94
+ res = http.request(request)
95
+ rescue Net::OpenTimeout, Net::ReadTimeout, Net::WriteTimeout, Timeout::Error, Errno::ETIMEDOUT, IOError => _e
96
+ # Timeout — retry
97
+ if index < max_retries - 1
98
+ sleep(sleep_duration)
99
+ request["hooksniff-retry-count"] = index + 1
100
+ next
101
+ else
102
+ raise
103
+ end
104
+ end
87
105
 
88
- [0.05, 0.1, 0.2].each_with_index do |sleep_duration, index|
89
106
  # 429 Rate Limit — respect Retry-After header
90
107
  if Integer(res.code) == 429
91
108
  retry_after = res["Retry-After"]
92
109
  delay = retry_after ? retry_after.to_f : sleep_duration
93
110
  sleep(delay)
94
111
  request["hooksniff-retry-count"] = index + 1
95
- res = http.request(request)
96
112
  next
97
113
  end
98
114
 
99
- unless Integer(res.code) >= 500
100
- break
115
+ # 5xx Server Error — exponential backoff
116
+ if Integer(res.code) >= 500 && index < max_retries - 1
117
+ sleep(sleep_duration)
118
+ request["hooksniff-retry-count"] = index + 1
119
+ next
101
120
  end
102
121
 
103
- sleep(sleep_duration)
104
- request["hooksniff-retry-count"] = index + 1
105
- res = http.request(request)
122
+ break
106
123
  end
107
124
 
108
125
  res
@@ -126,3 +143,25 @@ module HookSniff
126
143
  end
127
144
  end
128
145
  end
146
+
147
+ # Response metadata accessor
148
+ module HookSniff
149
+ class << self
150
+ attr_accessor :last_response
151
+ end
152
+ end
153
+
154
+ # Debug logging helper
155
+ module HookSniff
156
+ module DebugLogger
157
+ def self.log_request(method, url)
158
+ return unless HookSniff.respond_to?(:debug) && HookSniff.debug
159
+ puts "[HookSniff] → #{method.upcase} #{url}"
160
+ end
161
+
162
+ def self.log_response(status_code, elapsed_ms)
163
+ return unless HookSniff.respond_to?(:debug) && HookSniff.debug
164
+ puts "[HookSniff] ← #{status_code} (#{elapsed_ms}ms)"
165
+ end
166
+ end
167
+ end
@@ -0,0 +1,31 @@
1
+ # frozen_string_literal: true
2
+
3
+ module HookSniff
4
+ # Configuration options for the HookSniff client.
5
+ #
6
+ # @example
7
+ # options = HookSniff::Options.new(
8
+ # server_url: "https://custom.hooksniff.com",
9
+ # timeout: 60,
10
+ # debug: true,
11
+ # headers: { "X-Custom" => "value" }
12
+ # )
13
+ # client = HookSniff::HookSniff.new("token", options)
14
+ class Options
15
+ attr_accessor :server_url, :timeout, :debug, :headers, :retry_schedule
16
+
17
+ def initialize(
18
+ server_url: nil,
19
+ timeout: 30,
20
+ debug: false,
21
+ headers: {},
22
+ retry_schedule: [1, 2, 4]
23
+ )
24
+ @server_url = server_url
25
+ @timeout = timeout
26
+ @debug = debug
27
+ @headers = headers || {}
28
+ @retry_schedule = retry_schedule
29
+ end
30
+ end
31
+ end
@@ -0,0 +1,45 @@
1
+ # Pagination Helper for HookSniff Ruby SDK.
2
+ #
3
+ # Usage:
4
+ # hs.message.list_all(limit: 100).each do |msg|
5
+ # puts msg.id
6
+ # end
7
+ #
8
+ # # Or collect all
9
+ # all_messages = hs.message.list_all(limit: 100).to_a
10
+
11
+ module HookSniff
12
+ class Paginator
13
+ include Enumerable
14
+
15
+ def initialize(fetch_page, limit: nil)
16
+ @fetch_page = fetch_page
17
+ @limit = limit
18
+ end
19
+
20
+ def each(&block)
21
+ return enum_for(:each) unless block_given?
22
+
23
+ iterator = nil
24
+
25
+ loop do
26
+ page = @fetch_page.call(limit: @limit, iterator: iterator)
27
+
28
+ page.data.each(&block)
29
+
30
+ break if page.done || page.iterator.nil? || page.iterator.empty?
31
+ iterator = page.iterator
32
+ end
33
+ end
34
+
35
+ # Collect all items into an array
36
+ def to_a
37
+ each.to_a
38
+ end
39
+
40
+ # Count all items
41
+ def count
42
+ each.count
43
+ end
44
+ end
45
+ end
@@ -0,0 +1,48 @@
1
+ # frozen_string_literal: true
2
+
3
+ module HookSniff
4
+ # Response metadata from the last API request.
5
+ #
6
+ # Access via +client.last_response+ after any API call.
7
+ #
8
+ # @example
9
+ # endpoints = client.endpoint.list
10
+ # puts client.last_response.request_id
11
+ # puts client.last_response.rate_limit_remaining
12
+ class ResponseMetadata
13
+ # @return [Integer] HTTP status code
14
+ attr_reader :status_code
15
+
16
+ # @return [String, nil] x-request-id header
17
+ attr_reader :request_id
18
+
19
+ # @return [Integer, nil] x-ratelimit-remaining header
20
+ attr_reader :rate_limit_remaining
21
+
22
+ # @return [Integer, nil] x-ratelimit-reset header (Unix timestamp)
23
+ attr_reader :rate_limit_reset
24
+
25
+ # @return [Hash] All response headers
26
+ attr_reader :headers
27
+
28
+ def initialize(status_code:, request_id: nil, rate_limit_remaining: nil, rate_limit_reset: nil, headers: {})
29
+ @status_code = status_code
30
+ @request_id = request_id
31
+ @rate_limit_remaining = rate_limit_remaining
32
+ @rate_limit_reset = rate_limit_reset
33
+ @headers = headers
34
+ end
35
+
36
+ # Create from a Net::HTTP response.
37
+ def self.from_http_response(response)
38
+ headers = response.to_hash.transform_values { |v| v.is_a?(Array) ? v.first : v }
39
+ new(
40
+ status_code: response.code.to_i,
41
+ request_id: headers["x-request-id"],
42
+ rate_limit_remaining: headers["x-ratelimit-remaining"]&.to_i,
43
+ rate_limit_reset: headers["x-ratelimit-reset"]&.to_i,
44
+ headers: headers
45
+ )
46
+ end
47
+ end
48
+ end
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module HookSniff
4
- VERSION = "1.2.0"
4
+ VERSION = "1.3.0"
5
5
  end