rerout 0.1.0 → 0.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: f45e9af9b2dfce2933563d97fc0adca857d4cdca00a09fdafe89240fcdc1671f
4
- data.tar.gz: ed1bbdcea909d9be88f5af98fccfcf4090de2ce1d3f30fb060ee81bc091f6369
3
+ metadata.gz: 6f212d64e3f65e342bc6034aa992c270a2322534dc127e18fa4b7defbd6e0b08
4
+ data.tar.gz: bb2fd2083df69c90e884769fe5aad80b40d11dde7ba7696d96bc04ac73393833
5
5
  SHA512:
6
- metadata.gz: 974f74ad677142b918c0d9c367a785f6c2ae6198122ebaf75ee7d6f55e2b698883cbb01636f976d838bec3346cd96bc02e6da7b36da6adf92f49985feeb09650
7
- data.tar.gz: cdc59d4419f34a6e92ccc0765c6aeb03a773c1776bf966a8858bbfc11ea584a5349fdc16e0018fcf7f6aab6dbec93ce250de36dfab849f3f644561ecd1baaa77
6
+ metadata.gz: a86ed8c18689fa52099e11a1fc1bedec3ddd2aa5897ff2d9c694290260892712591bccc61b1aee0129e24e7f2182b5d5dc7657126dfb9960e9cfb6ad3229b79d
7
+ data.tar.gz: 812e84cfb86041e00b6798bd149176818162d7c25f56267dc1be12a7a7c4f5c1c8e816a0744194c0b9ce752619bc090deef52f7742a594a8f6e3ae77f55feb07
data/CHANGELOG.md CHANGED
@@ -4,6 +4,28 @@ All notable changes to the `rerout` gem are documented in this file. The
4
4
  format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
5
5
  and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
6
6
 
7
+ ## [0.3.0] - 2026-06-03
8
+
9
+ ### Added
10
+
11
+ - Webhook endpoint management via a new `webhooks` namespace — `create`, `list`,
12
+ and `delete` against `/v1/projects/me/webhooks` (API-key auth). The signing
13
+ secret returned by `create` is shown once.
14
+
15
+ ## [0.2.0] - 2026-06-02
16
+
17
+ ### Added
18
+
19
+ - Read-only `tags` field on `Rerout::Models::Link` — an array of
20
+ `Rerout::Models::Tag` value objects (`{ id, name, color }`). Populated on
21
+ `get`, `list`, and `update`; an empty array on `create`. Tag writes are not
22
+ supported for API-key clients.
23
+
24
+ ### Notes
25
+
26
+ - The project stats endpoint `/v1/projects/me/stats` (used by
27
+ `project.stats`) is now live.
28
+
7
29
  ## [0.1.0] - 2026-05-20
8
30
 
9
31
  ### Added
@@ -26,4 +48,6 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
26
48
  - `Rerout::Error` with stable `code`, `status`, `path`, `timestamp`, `details`
27
49
  plus `rate_limited?` and `server_error?` convenience flags.
28
50
 
51
+ [0.3.0]: https://github.com/ModestNerds-Co/rerout-sdks/releases/tag/ruby/v0.3.0
52
+ [0.2.0]: https://github.com/ModestNerds-Co/rerout-sdks/releases/tag/ruby-v0.2.0
29
53
  [0.1.0]: https://github.com/ModestNerds-Co/rerout-sdks/releases/tag/ruby-v0.1.0
data/README.md CHANGED
@@ -64,7 +64,7 @@ rerout = Rerout::Client.new(
64
64
  A blank or missing `api_key` raises `Rerout::Error` with code `missing_api_key`
65
65
  before any network call.
66
66
 
67
- The client exposes three namespaces: `links`, `project`, and `qr`.
67
+ The client exposes four namespaces: `links`, `project`, `qr`, and `webhooks`.
68
68
 
69
69
  ## Links
70
70
 
@@ -82,6 +82,7 @@ page = rerout.links.list(cursor: page.next_cursor) if page.next_cursor
82
82
 
83
83
  # Get one
84
84
  link = rerout.links.get('q4')
85
+ link.tags # => [Rerout::Models::Tag, ...] — read-only { id, name, color }
85
86
 
86
87
  # Update — only the fields you set are sent.
87
88
  rerout.links.update('q4', Rerout::UpdateLinkInput.new(is_active: false))
@@ -146,6 +147,38 @@ File.write('q4.svg', svg)
146
147
  QR options: `size` (1–32), `margin` (0–16), `ecc` (`L`/`M`/`Q`/`H`), `domain`,
147
148
  and `refresh` (`true` is serialized as `1`; a string is sent verbatim).
148
149
 
150
+ ## Webhook management
151
+
152
+ Manage the webhook endpoints that receive event deliveries for the project that
153
+ owns the API key. (This is the `webhooks` namespace — distinct from
154
+ `Rerout::Webhooks`, which verifies inbound signatures below.)
155
+
156
+ ```ruby
157
+ # Create — name, url, and events are required.
158
+ created = rerout.webhooks.create(
159
+ Rerout::CreateWebhookInput.new(
160
+ name: 'prod listener',
161
+ url: 'https://hooks.brand.com/rerout',
162
+ events: ['link.created'],
163
+ payload_format: 'json' # optional: "json" (default) or "slack"
164
+ )
165
+ )
166
+
167
+ created.endpoint.id # => "wh_..."
168
+ created.signing_secret # => "whsec_..." — shown ONCE; persist it now.
169
+
170
+ # List endpoints plus the event types the server can deliver.
171
+ result = rerout.webhooks.list
172
+ result.endpoints # => [Rerout::Models::Webhook, ...]
173
+ result.event_types # => ["link.created", ...]
174
+
175
+ # Delete (soft delete) — idempotent.
176
+ rerout.webhooks.delete('wh_...') # => { "deleted" => true }
177
+ ```
178
+
179
+ The `signing_secret` returned by `create` is the value you pass as `secret:` to
180
+ `verify_signature` below — store it securely, as it cannot be retrieved again.
181
+
149
182
  ## Webhook signature verification
150
183
 
151
184
  Rerout signs every webhook delivery with an `X-Rerout-Signature` header. Verify
data/lib/rerout/client.rb CHANGED
@@ -7,12 +7,14 @@ require_relative 'version'
7
7
  require_relative 'error'
8
8
  require_relative 'create_link_input'
9
9
  require_relative 'update_link_input'
10
+ require_relative 'create_webhook_input'
10
11
  require_relative 'qr_options'
11
12
  require_relative 'webhooks'
12
13
  require_relative 'models'
13
14
  require_relative 'links'
14
15
  require_relative 'project'
15
16
  require_relative 'qr'
17
+ require_relative 'webhooks_resource'
16
18
 
17
19
  module Rerout
18
20
  # Default production API base URL.
@@ -36,6 +38,8 @@ module Rerout
36
38
  attr_reader :project
37
39
  # @return [Resources::Qr] QR namespace.
38
40
  attr_reader :qr
41
+ # @return [Resources::Webhooks] webhook endpoint management namespace.
42
+ attr_reader :webhooks
39
43
 
40
44
  # @param api_key [String] project API key (`rrk_…`). Required.
41
45
  # @param base_url [String, nil] override base URL. Defaults to `https://api.rerout.co`.
@@ -61,6 +65,7 @@ module Rerout
61
65
  @links = Resources::Links.new(self)
62
66
  @project = Resources::Project.new(self)
63
67
  @qr = Resources::Qr.new(self)
68
+ @webhooks = Resources::Webhooks.new(self)
64
69
  end
65
70
 
66
71
  # Perform a JSON request against the Rerout API.
@@ -0,0 +1,41 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Rerout
4
+ # Request body for `POST /v1/projects/me/webhooks`. `name`, `url`, and
5
+ # `events` are required; `is_active` and `payload_format` are optional and
6
+ # omitted from the payload when not set (server defaults apply).
7
+ class CreateWebhookInput
8
+ attr_reader :name, :url, :events, :is_active, :payload_format
9
+
10
+ # @param name [String] required, human-readable label for the endpoint.
11
+ # @param url [String] required, public https:// URL that receives deliveries.
12
+ # @param events [Array<String>] required, non-empty list of event types
13
+ # to subscribe to (e.g. `link.created`).
14
+ # @param is_active [Boolean, nil] whether the endpoint starts active.
15
+ # Server default: `true`.
16
+ # @param payload_format [String, nil] payload encoding — `"json"` or
17
+ # `"slack"`. Server default: `"json"`.
18
+ def initialize(name:, url:, events:, is_active: nil, payload_format: nil)
19
+ raise ArgumentError, 'name is required' if name.nil? || name.to_s.empty?
20
+ raise ArgumentError, 'url is required' if url.nil? || url.to_s.empty?
21
+ if events.nil? || !events.is_a?(Array) || events.empty?
22
+ raise ArgumentError, 'events is required and must be a non-empty Array'
23
+ end
24
+
25
+ @name = name
26
+ @url = url
27
+ @events = events
28
+ @is_active = is_active
29
+ @payload_format = payload_format
30
+ freeze
31
+ end
32
+
33
+ # Serialize for the wire. Optional fields are only included when set.
34
+ def to_h
35
+ hash = { 'name' => name, 'url' => url, 'events' => events }
36
+ hash['is_active'] = is_active unless is_active.nil?
37
+ hash['payload_format'] = payload_format unless payload_format.nil?
38
+ hash
39
+ end
40
+ end
41
+ end
data/lib/rerout/models.rb CHANGED
@@ -5,18 +5,49 @@ module Rerout
5
5
  # frozen struct-like class with `from_hash` for JSON ingestion and value
6
6
  # equality semantics.
7
7
  module Models
8
+ # A label attached to a link — `{ id:, name:, color: }`. Read-only; the API
9
+ # ignores tag writes for API-key clients.
10
+ class Tag
11
+ attr_reader :id, :name, :color
12
+
13
+ def initialize(id:, name:, color:)
14
+ @id = id
15
+ @name = name
16
+ @color = color
17
+ freeze
18
+ end
19
+
20
+ def self.from_hash(hash)
21
+ new(id: hash['id'], name: hash['name'], color: hash['color'])
22
+ end
23
+
24
+ def to_h
25
+ { id: id, name: name, color: color }
26
+ end
27
+
28
+ def ==(other)
29
+ other.is_a?(Tag) && other.id == id && other.name == name && other.color == color
30
+ end
31
+ alias eql? ==
32
+
33
+ def hash
34
+ [self.class, id, name, color].hash
35
+ end
36
+ end
37
+
8
38
  # A short link.
9
39
  class Link
10
40
  ATTRS = %i[
11
41
  code short_url domain_hostname target_url project_id expires_at
12
42
  is_active seo_title seo_description seo_image_url seo_canonical_url
13
- seo_noindex seo_updated_at created_at updated_at
43
+ seo_noindex seo_updated_at tags created_at updated_at
14
44
  ].freeze
15
45
 
16
46
  attr_reader(*ATTRS)
17
47
 
18
48
  def initialize(**attrs)
19
49
  ATTRS.each { |k| instance_variable_set(:"@#{k}", attrs[k]) }
50
+ @tags = (@tags || []).freeze
20
51
  freeze
21
52
  end
22
53
 
@@ -35,13 +66,16 @@ module Rerout
35
66
  seo_canonical_url: hash['seo_canonical_url'],
36
67
  seo_noindex: hash.fetch('seo_noindex', true),
37
68
  seo_updated_at: hash['seo_updated_at'],
69
+ tags: (hash['tags'] || []).map { |t| Tag.from_hash(t) },
38
70
  created_at: hash['created_at'],
39
71
  updated_at: hash['updated_at']
40
72
  )
41
73
  end
42
74
 
43
75
  def to_h
44
- ATTRS.to_h { |k| [k, public_send(k)] }
76
+ ATTRS.to_h do |k|
77
+ [k, k == :tags ? tags.map(&:to_h) : public_send(k)]
78
+ end
45
79
  end
46
80
 
47
81
  def ==(other)
@@ -267,5 +301,115 @@ module Rerout
267
301
  [self.class, id, name, slug].hash
268
302
  end
269
303
  end
304
+
305
+ # A webhook endpoint registered to the project. Mirrors the server-side
306
+ # `WebhookEndpointResponse`.
307
+ class Webhook
308
+ ATTRS = %i[
309
+ id project_id name url events is_active payload_format
310
+ created_at updated_at last_delivery_at last_success_at last_failure_at
311
+ ].freeze
312
+
313
+ attr_reader(*ATTRS)
314
+
315
+ def initialize(**attrs)
316
+ ATTRS.each { |k| instance_variable_set(:"@#{k}", attrs[k]) }
317
+ @events = (@events || []).freeze
318
+ freeze
319
+ end
320
+
321
+ def self.from_hash(hash)
322
+ new(
323
+ id: hash['id'],
324
+ project_id: hash['project_id'],
325
+ name: hash['name'],
326
+ url: hash['url'],
327
+ events: hash['events'] || [],
328
+ is_active: hash['is_active'],
329
+ payload_format: hash['payload_format'],
330
+ created_at: hash['created_at'],
331
+ updated_at: hash['updated_at'],
332
+ last_delivery_at: hash['last_delivery_at'],
333
+ last_success_at: hash['last_success_at'],
334
+ last_failure_at: hash['last_failure_at']
335
+ )
336
+ end
337
+
338
+ def to_h
339
+ ATTRS.to_h { |k| [k, public_send(k)] }
340
+ end
341
+
342
+ def ==(other)
343
+ other.is_a?(Webhook) && other.to_h == to_h
344
+ end
345
+ alias eql? ==
346
+
347
+ def hash
348
+ to_h.hash
349
+ end
350
+ end
351
+
352
+ # Result of creating a webhook. The `signing_secret` (`whsec_…`) is
353
+ # returned **once** — store it now; it is never shown again.
354
+ class CreatedWebhook
355
+ attr_reader :endpoint, :signing_secret
356
+
357
+ def initialize(endpoint:, signing_secret:)
358
+ @endpoint = endpoint
359
+ @signing_secret = signing_secret
360
+ freeze
361
+ end
362
+
363
+ def self.from_hash(hash)
364
+ new(
365
+ endpoint: Webhook.from_hash(hash['endpoint'] || {}),
366
+ signing_secret: hash['signing_secret']
367
+ )
368
+ end
369
+
370
+ def to_h
371
+ { endpoint: endpoint.to_h, signing_secret: signing_secret }
372
+ end
373
+
374
+ def ==(other)
375
+ other.is_a?(CreatedWebhook) && other.to_h == to_h
376
+ end
377
+ alias eql? ==
378
+
379
+ def hash
380
+ to_h.hash
381
+ end
382
+ end
383
+
384
+ # List of webhook endpoints plus every event type the server can deliver.
385
+ class ListWebhooksResult
386
+ attr_reader :endpoints, :event_types
387
+
388
+ def initialize(endpoints:, event_types:)
389
+ @endpoints = endpoints.freeze
390
+ @event_types = event_types.freeze
391
+ freeze
392
+ end
393
+
394
+ def self.from_hash(hash)
395
+ new(
396
+ endpoints: (hash['endpoints'] || []).map { |e| Webhook.from_hash(e) },
397
+ event_types: hash['event_types'] || []
398
+ )
399
+ end
400
+
401
+ def to_h
402
+ { endpoints: endpoints.map(&:to_h), event_types: event_types }
403
+ end
404
+
405
+ def ==(other)
406
+ other.is_a?(ListWebhooksResult) && other.to_h == to_h
407
+ end
408
+ alias eql? ==
409
+
410
+ def hash
411
+ to_h.hash
412
+ end
413
+ end
270
414
  end
271
415
  end
@@ -2,5 +2,5 @@
2
2
 
3
3
  module Rerout
4
4
  # Library version. Follows semantic versioning.
5
- VERSION = '0.1.0'
5
+ VERSION = '0.3.0'
6
6
  end
@@ -0,0 +1,63 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'erb'
4
+
5
+ module Rerout
6
+ module Resources
7
+ # Webhook endpoint management namespace — create, list, delete endpoints
8
+ # for the project that owns the API key.
9
+ #
10
+ # This is distinct from {Rerout::Webhooks}, which verifies *inbound*
11
+ # delivery signatures. Reach it via `client.webhooks`.
12
+ class Webhooks
13
+ # @param client [Rerout::Client]
14
+ def initialize(client)
15
+ @client = client
16
+ end
17
+
18
+ # Create a webhook endpoint. The returned `signing_secret` (`whsec_…`) is
19
+ # shown once — persist it to verify future deliveries.
20
+ #
21
+ # @param input [Rerout::CreateWebhookInput, Hash] the request body.
22
+ # @return [Rerout::Models::CreatedWebhook]
23
+ def create(input)
24
+ body = coerce_input(input)
25
+ response = @client.request(method: :post, path: '/v1/projects/me/webhooks', body: body)
26
+ Models::CreatedWebhook.from_hash(response)
27
+ end
28
+
29
+ # List webhook endpoints and the event types the server can deliver.
30
+ #
31
+ # @return [Rerout::Models::ListWebhooksResult]
32
+ def list
33
+ response = @client.request(method: :get, path: '/v1/projects/me/webhooks')
34
+ Models::ListWebhooksResult.from_hash(response)
35
+ end
36
+
37
+ # Soft-delete an endpoint and abandon its pending deliveries. Idempotent.
38
+ #
39
+ # @param endpoint_id [String] the endpoint id (`wh_…`).
40
+ # @return [Hash] `{ "deleted" => true }`
41
+ def delete(endpoint_id)
42
+ @client.request(method: :delete, path: webhook_path(endpoint_id))
43
+ end
44
+
45
+ private
46
+
47
+ def webhook_path(endpoint_id)
48
+ raise ArgumentError, 'endpoint_id is required' if endpoint_id.nil? || endpoint_id.to_s.empty?
49
+
50
+ "/v1/projects/me/webhooks/#{ERB::Util.url_encode(endpoint_id.to_s)}"
51
+ end
52
+
53
+ def coerce_input(input)
54
+ case input
55
+ when CreateWebhookInput then input.to_h
56
+ when Hash then input
57
+ else
58
+ raise ArgumentError, 'input must be a Rerout::CreateWebhookInput or Hash'
59
+ end
60
+ end
61
+ end
62
+ end
63
+ end
data/lib/rerout.rb CHANGED
@@ -5,11 +5,13 @@ require_relative 'rerout/error'
5
5
  require_relative 'rerout/models'
6
6
  require_relative 'rerout/create_link_input'
7
7
  require_relative 'rerout/update_link_input'
8
+ require_relative 'rerout/create_webhook_input'
8
9
  require_relative 'rerout/qr_options'
9
10
  require_relative 'rerout/webhooks'
10
11
  require_relative 'rerout/links'
11
12
  require_relative 'rerout/project'
12
13
  require_relative 'rerout/qr'
14
+ require_relative 'rerout/webhooks_resource'
13
15
  require_relative 'rerout/client'
14
16
 
15
17
  # Official Ruby SDK for the Rerout branded-link API.
metadata CHANGED
@@ -1,13 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: rerout
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.1.0
4
+ version: 0.3.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Codecraft Solutions
8
+ autorequire:
8
9
  bindir: bin
9
10
  cert_chain: []
10
- date: 1980-01-02 00:00:00.000000000 Z
11
+ date: 2026-06-03 00:00:00.000000000 Z
11
12
  dependencies:
12
13
  - !ruby/object:Gem::Dependency
13
14
  name: faraday
@@ -109,6 +110,7 @@ files:
109
110
  - lib/rerout.rb
110
111
  - lib/rerout/client.rb
111
112
  - lib/rerout/create_link_input.rb
113
+ - lib/rerout/create_webhook_input.rb
112
114
  - lib/rerout/error.rb
113
115
  - lib/rerout/links.rb
114
116
  - lib/rerout/models.rb
@@ -118,6 +120,7 @@ files:
118
120
  - lib/rerout/update_link_input.rb
119
121
  - lib/rerout/version.rb
120
122
  - lib/rerout/webhooks.rb
123
+ - lib/rerout/webhooks_resource.rb
121
124
  homepage: https://github.com/ModestNerds-Co/rerout-sdks
122
125
  licenses:
123
126
  - MIT
@@ -128,6 +131,7 @@ metadata:
128
131
  bug_tracker_uri: https://github.com/ModestNerds-Co/rerout-sdks/issues
129
132
  documentation_uri: https://rerout.co/docs
130
133
  rubygems_mfa_required: 'true'
134
+ post_install_message:
131
135
  rdoc_options: []
132
136
  require_paths:
133
137
  - lib
@@ -142,7 +146,8 @@ required_rubygems_version: !ruby/object:Gem::Requirement
142
146
  - !ruby/object:Gem::Version
143
147
  version: '0'
144
148
  requirements: []
145
- rubygems_version: 3.6.9
149
+ rubygems_version: 3.0.3.1
150
+ signing_key:
146
151
  specification_version: 4
147
152
  summary: Official Ruby SDK for the Rerout branded-link API.
148
153
  test_files: []