rerout 0.2.0 → 0.5.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: 78cf1ae4573eccdeed6874c837565ea69f415c928c23def1fe506e674be53ec3
4
- data.tar.gz: aa23cc62bcd1c3f2f80ee460fe691d0241027020e5d7f5d3bfacb2e909263286
3
+ metadata.gz: 4d7253a49e955b1812e46e3d0c435999fda4c7276d5f19bad47586ac56d06247
4
+ data.tar.gz: aeb26215de9a5c1e3f5de9637d348342f7cac7461179a8db7b005a438d1260ef
5
5
  SHA512:
6
- metadata.gz: 4f1e411e4301abfd0009626920e3e974ca4f2b7db05de0d930e2eb9d432b3d71e8c51b5185875c04fdc8edd97cf1cc756e93215cc24e82103086f475714732e0
7
- data.tar.gz: 1722814f198aca44eded94a7f5c983a7f30a9437a75652046ad61226cd454d005b248fe500a68080c4aa5b98fc0089274fe610089a3ac8f5b2a68832fcfb723b
6
+ metadata.gz: 1a4435295c93cc7e74812509c38d5cd7c829e004a37ec71796b3cb08b3707d7959ea4ba8d28aef0925d58bb48b00b39f8df807f4503af1fd33d880de3c4ffc04
7
+ data.tar.gz: ae6b3a652f246e38ca8537d4eda5699733de2fd3dcc1ed2e102eaca3e6e61041c1d8344082da8c764cdc35aeaa7c202c2818d6f0ed223bc10df376fdd5f658e6
data/CHANGELOG.md CHANGED
@@ -4,6 +4,53 @@ 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.5.0] - 2026-06-20
8
+
9
+ ### Added
10
+
11
+ - New `tags` namespace for tag management — `client.tags.list`, `create`,
12
+ `update(tag_id, input)`, and `delete(tag_id)` against `/v1/projects/me/tags`
13
+ (API-key auth; project resolved from the key).
14
+ - `Rerout::CreateTagInput` (required `name`, optional `color`) and
15
+ `Rerout::UpdateTagInput` (both fields optional; omitted fields are left
16
+ unchanged, no client-side empty-payload check) request bodies. Both also
17
+ accept a plain Hash.
18
+ - New value models `Rerout::Models::TagSummary` (`Tag` plus `link_count`, the
19
+ list-response shape) and `Rerout::Models::ListTagsResult`. `tags.create` /
20
+ `tags.update` return the existing `Rerout::Models::Tag` (`{ id, name, color }`).
21
+
22
+ ## [0.4.0] - 2026-06-04
23
+
24
+ ### Added
25
+
26
+ - Smart Link fields on `Rerout::Models::Link`: `password_protected`,
27
+ `max_clicks`, `click_count`, `track_conversions`, `routing_rules` (array of
28
+ `RoutingRule`), and `ab_variants` (array of `AbVariant`).
29
+ - `Rerout::CreateLinkInput` gains optional `password`, `max_clicks`,
30
+ `track_conversions`, `routing_rules`, and `ab_variants`.
31
+ - `Rerout::UpdateLinkInput` gains `password`, `max_clicks`,
32
+ `track_conversions`, `routing_rules`, and `ab_variants`. `password` and
33
+ `max_clicks` accept `Rerout::CLEAR` to send `null`; `routing_rules` and
34
+ `ab_variants` are a full replacement.
35
+ - New `Rerout::Models::RoutingRule` and `Rerout::Models::AbVariant` value
36
+ objects. Routing-rule `condition_type` is `"country"` / `"device"`;
37
+ `condition_op` is `"is"` / `"is_not"` / `"in"`.
38
+ - New `conversions` namespace — `client.conversions.record(click_id,
39
+ event_name, value_cents:, currency:)` against `POST /v1/conversions`,
40
+ returning a `Rerout::Models::RecordedConversion` (`recorded`, `duplicate`).
41
+ - New `client.links.create_batch(inputs)` against `POST /v1/links/batch`,
42
+ returning a `Rerout::Models::BatchCreateLinksResult` (`created`, `total`,
43
+ `results` of `BatchLinkResult`).
44
+ - New webhook event type `conversion.recorded`.
45
+
46
+ ## [0.3.0] - 2026-06-03
47
+
48
+ ### Added
49
+
50
+ - Webhook endpoint management via a new `webhooks` namespace — `create`, `list`,
51
+ and `delete` against `/v1/projects/me/webhooks` (API-key auth). The signing
52
+ secret returned by `create` is shown once.
53
+
7
54
  ## [0.2.0] - 2026-06-02
8
55
 
9
56
  ### Added
@@ -40,5 +87,6 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
40
87
  - `Rerout::Error` with stable `code`, `status`, `path`, `timestamp`, `details`
41
88
  plus `rate_limited?` and `server_error?` convenience flags.
42
89
 
90
+ [0.3.0]: https://github.com/ModestNerds-Co/rerout-sdks/releases/tag/ruby/v0.3.0
43
91
  [0.2.0]: https://github.com/ModestNerds-Co/rerout-sdks/releases/tag/ruby-v0.2.0
44
92
  [0.1.0]: https://github.com/ModestNerds-Co/rerout-sdks/releases/tag/ruby-v0.1.0
data/README.md CHANGED
@@ -64,7 +64,8 @@ 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 these namespaces: `links`, `project`, `qr`, `webhooks`,
68
+ `conversions`, and `tags`.
68
69
 
69
70
  ## Links
70
71
 
@@ -111,6 +112,99 @@ rerout.links.update('q4', Rerout::UpdateLinkInput.new(target_url: 'https://new.e
111
112
  An `UpdateLinkInput` with no fields set raises `Rerout::Error` (code
112
113
  `empty_update`) client-side without hitting the API.
113
114
 
115
+ ## Smart Links
116
+
117
+ Links carry Smart Link configuration: `password_protected`, `max_clicks`,
118
+ `click_count`, `track_conversions`, plus `routing_rules` (array of
119
+ `Rerout::Models::RoutingRule`) and `ab_variants` (array of
120
+ `Rerout::Models::AbVariant`).
121
+
122
+ ```ruby
123
+ rerout.links.create(
124
+ Rerout::CreateLinkInput.new(
125
+ target_url: 'https://example.com',
126
+ password: 'hunter2',
127
+ max_clicks: 500,
128
+ track_conversions: true,
129
+ routing_rules: [
130
+ Rerout::Models::RoutingRule.new(
131
+ condition_type: 'country', # "country" | "device"
132
+ condition_op: 'in', # "is" | "is_not" | "in"
133
+ condition_value: 'US,CA',
134
+ target_url: 'https://example.com/na'
135
+ )
136
+ ],
137
+ ab_variants: [
138
+ Rerout::Models::AbVariant.new(target_url: 'https://example.com/a', weight: 70),
139
+ Rerout::Models::AbVariant.new(target_url: 'https://example.com/b', weight: 30)
140
+ ]
141
+ )
142
+ )
143
+
144
+ # On update, password/max_clicks accept Rerout::CLEAR to null them;
145
+ # routing_rules and ab_variants fully replace the existing config.
146
+ rerout.links.update('q4', Rerout::UpdateLinkInput.new(password: Rerout::CLEAR, max_clicks: Rerout::CLEAR))
147
+ rerout.links.update('q4', Rerout::UpdateLinkInput.new(routing_rules: [], ab_variants: []))
148
+ ```
149
+
150
+ Routing rules and A/B variants also accept plain Hashes.
151
+
152
+ ## Batch link creation
153
+
154
+ ```ruby
155
+ result = rerout.links.create_batch(
156
+ [
157
+ Rerout::CreateLinkInput.new(target_url: 'https://example.com/1'),
158
+ Rerout::CreateLinkInput.new(target_url: 'https://example.com/2', code: 'promo')
159
+ ]
160
+ )
161
+ result.created # => 2
162
+ result.total # => 2
163
+ result.results.each { |r| puts [r.index, r.ok, r.code || r.error].inspect }
164
+ ```
165
+
166
+ The batch endpoint accepts `target_url`, `code`, `expires_at`, and
167
+ `domain_hostname` per link; other fields are ignored.
168
+
169
+ ## Conversions
170
+
171
+ ```ruby
172
+ result = rerout.conversions.record('clk_123', 'purchase', value_cents: 4999, currency: 'USD')
173
+ result.recorded # => true
174
+ result.duplicate # => false
175
+ ```
176
+
177
+ `value_cents` and `currency` are optional. The call is idempotent — a repeat
178
+ for the same click + event returns `duplicate: true`.
179
+
180
+ ## Tags
181
+
182
+ Manage the tags that can be attached to links for the project that owns the API
183
+ key. (Links carry their tags read-only as `link.tags`; this namespace lets you
184
+ list, create, update, and delete them.)
185
+
186
+ ```ruby
187
+ # List — each tag carries its live (non-deleted) link count.
188
+ result = rerout.tags.list
189
+ result.tags # => [Rerout::Models::TagSummary, ...] — { id, name, color, link_count }
190
+ result.tags.first.link_count # => 4
191
+
192
+ # Create — name is required; color is optional (server defaults to "teal").
193
+ tag = rerout.tags.create(Rerout::CreateTagInput.new(name: 'Spring 2026', color: 'teal'))
194
+ tag # => Rerout::Models::Tag — { id, name, color }
195
+
196
+ # Update — only the fields you set are sent; omitted fields are left unchanged.
197
+ rerout.tags.update(tag.id, Rerout::UpdateTagInput.new(name: 'Renamed'))
198
+ rerout.tags.update(tag.id, Rerout::UpdateTagInput.new(color: 'red'))
199
+
200
+ # Delete — also drops the tag from every link it was attached to.
201
+ rerout.tags.delete(tag.id) # => { "deleted" => true }
202
+ ```
203
+
204
+ `CreateTagInput` and `UpdateTagInput` also accept plain Hashes. An
205
+ `UpdateTagInput` with no fields set sends an empty body; the server rejects a
206
+ fully empty patch with HTTP `400`.
207
+
114
208
  ## Project
115
209
 
116
210
  ```ruby
@@ -147,6 +241,38 @@ File.write('q4.svg', svg)
147
241
  QR options: `size` (1–32), `margin` (0–16), `ecc` (`L`/`M`/`Q`/`H`), `domain`,
148
242
  and `refresh` (`true` is serialized as `1`; a string is sent verbatim).
149
243
 
244
+ ## Webhook management
245
+
246
+ Manage the webhook endpoints that receive event deliveries for the project that
247
+ owns the API key. (This is the `webhooks` namespace — distinct from
248
+ `Rerout::Webhooks`, which verifies inbound signatures below.)
249
+
250
+ ```ruby
251
+ # Create — name, url, and events are required.
252
+ created = rerout.webhooks.create(
253
+ Rerout::CreateWebhookInput.new(
254
+ name: 'prod listener',
255
+ url: 'https://hooks.brand.com/rerout',
256
+ events: ['link.created'],
257
+ payload_format: 'json' # optional: "json" (default) or "slack"
258
+ )
259
+ )
260
+
261
+ created.endpoint.id # => "wh_..."
262
+ created.signing_secret # => "whsec_..." — shown ONCE; persist it now.
263
+
264
+ # List endpoints plus the event types the server can deliver.
265
+ result = rerout.webhooks.list
266
+ result.endpoints # => [Rerout::Models::Webhook, ...]
267
+ result.event_types # => ["link.created", ...]
268
+
269
+ # Delete (soft delete) — idempotent.
270
+ rerout.webhooks.delete('wh_...') # => { "deleted" => true }
271
+ ```
272
+
273
+ The `signing_secret` returned by `create` is the value you pass as `secret:` to
274
+ `verify_signature` below — store it securely, as it cannot be retrieved again.
275
+
150
276
  ## Webhook signature verification
151
277
 
152
278
  Rerout signs every webhook delivery with an `X-Rerout-Signature` header. Verify
data/lib/rerout/client.rb CHANGED
@@ -5,14 +5,21 @@ require 'json'
5
5
 
6
6
  require_relative 'version'
7
7
  require_relative 'error'
8
+ require_relative 'models'
9
+ require_relative 'input_serialization'
8
10
  require_relative 'create_link_input'
9
11
  require_relative 'update_link_input'
12
+ require_relative 'create_webhook_input'
13
+ require_relative 'create_tag_input'
14
+ require_relative 'update_tag_input'
10
15
  require_relative 'qr_options'
11
16
  require_relative 'webhooks'
12
- require_relative 'models'
13
17
  require_relative 'links'
14
18
  require_relative 'project'
15
19
  require_relative 'qr'
20
+ require_relative 'webhooks_resource'
21
+ require_relative 'conversions_resource'
22
+ require_relative 'tags_resource'
16
23
 
17
24
  module Rerout
18
25
  # Default production API base URL.
@@ -36,6 +43,12 @@ module Rerout
36
43
  attr_reader :project
37
44
  # @return [Resources::Qr] QR namespace.
38
45
  attr_reader :qr
46
+ # @return [Resources::Webhooks] webhook endpoint management namespace.
47
+ attr_reader :webhooks
48
+ # @return [Resources::Conversions] conversion tracking namespace.
49
+ attr_reader :conversions
50
+ # @return [Resources::Tags] tag management namespace.
51
+ attr_reader :tags
39
52
 
40
53
  # @param api_key [String] project API key (`rrk_…`). Required.
41
54
  # @param base_url [String, nil] override base URL. Defaults to `https://api.rerout.co`.
@@ -61,6 +74,9 @@ module Rerout
61
74
  @links = Resources::Links.new(self)
62
75
  @project = Resources::Project.new(self)
63
76
  @qr = Resources::Qr.new(self)
77
+ @webhooks = Resources::Webhooks.new(self)
78
+ @conversions = Resources::Conversions.new(self)
79
+ @tags = Resources::Tags.new(self)
64
80
  end
65
81
 
66
82
  # Perform a JSON request against the Rerout API.
@@ -0,0 +1,37 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Rerout
4
+ module Resources
5
+ # Conversion tracking namespace — record a conversion against a recorded
6
+ # click. Reach it via `client.conversions`.
7
+ class Conversions
8
+ # @param client [Rerout::Client]
9
+ def initialize(client)
10
+ @client = client
11
+ end
12
+
13
+ # Record a conversion for a click via `POST /v1/conversions`.
14
+ #
15
+ # @param click_id [String] the id of the click being converted.
16
+ # @param event_name [String] name of the conversion event (e.g. `"purchase"`).
17
+ # @param value_cents [Integer, nil] optional monetary value in the
18
+ # smallest currency unit (cents). Omitted when nil.
19
+ # @param currency [String, nil] optional ISO-4217 currency code
20
+ # (e.g. `"USD"`). Omitted when nil.
21
+ # @return [Rerout::Models::RecordedConversion] `recorded` + `duplicate`
22
+ # flags. Idempotent — a repeat for the same click + event returns
23
+ # `duplicate: true`.
24
+ def record(click_id, event_name, value_cents: nil, currency: nil)
25
+ raise ArgumentError, 'click_id is required' if click_id.nil? || click_id.to_s.empty?
26
+ raise ArgumentError, 'event_name is required' if event_name.nil? || event_name.to_s.empty?
27
+
28
+ body = { 'click_id' => click_id, 'event_name' => event_name }
29
+ body['value_cents'] = value_cents unless value_cents.nil?
30
+ body['currency'] = currency unless currency.nil?
31
+
32
+ response = @client.request(method: :post, path: '/v1/conversions', body: body)
33
+ Models::RecordedConversion.from_hash(response)
34
+ end
35
+ end
36
+ end
37
+ end
@@ -6,7 +6,9 @@ module Rerout
6
6
  class CreateLinkInput
7
7
  attr_reader :target_url, :domain_hostname, :code, :expires_at,
8
8
  :seo_title, :seo_description, :seo_image_url,
9
- :seo_canonical_url, :seo_noindex
9
+ :seo_canonical_url, :seo_noindex,
10
+ :password, :max_clicks, :track_conversions,
11
+ :routing_rules, :ab_variants
10
12
 
11
13
  # @param target_url [String] required, the destination URL.
12
14
  # @param domain_hostname [String, nil] verified custom domain (e.g. `go.brand.com`).
@@ -17,9 +19,18 @@ module Rerout
17
19
  # @param seo_image_url [String, nil] absolute https:// URL.
18
20
  # @param seo_canonical_url [String, nil]
19
21
  # @param seo_noindex [Boolean, nil] default server-side: `true`.
22
+ # @param password [String, nil] gate the link behind a password.
23
+ # @param max_clicks [Integer, nil] disable the link after this many clicks.
24
+ # @param track_conversions [Boolean, nil] enable conversion tracking.
25
+ # @param routing_rules [Array<Rerout::Models::RoutingRule, Hash>, nil]
26
+ # Smart Link routing rules.
27
+ # @param ab_variants [Array<Rerout::Models::AbVariant, Hash>, nil]
28
+ # A/B-test variants — each `{ target_url:, weight? }`.
20
29
  def initialize(target_url:, domain_hostname: nil, code: nil, expires_at: nil,
21
30
  seo_title: nil, seo_description: nil, seo_image_url: nil,
22
- seo_canonical_url: nil, seo_noindex: nil)
31
+ seo_canonical_url: nil, seo_noindex: nil,
32
+ password: nil, max_clicks: nil, track_conversions: nil,
33
+ routing_rules: nil, ab_variants: nil)
23
34
  raise ArgumentError, 'target_url is required' if target_url.nil? || target_url.to_s.empty?
24
35
 
25
36
  @target_url = target_url
@@ -31,6 +42,11 @@ module Rerout
31
42
  @seo_image_url = seo_image_url
32
43
  @seo_canonical_url = seo_canonical_url
33
44
  @seo_noindex = seo_noindex
45
+ @password = password
46
+ @max_clicks = max_clicks
47
+ @track_conversions = track_conversions
48
+ @routing_rules = routing_rules
49
+ @ab_variants = ab_variants
34
50
  freeze
35
51
  end
36
52
 
@@ -45,6 +61,11 @@ module Rerout
45
61
  hash['seo_image_url'] = seo_image_url unless seo_image_url.nil?
46
62
  hash['seo_canonical_url'] = seo_canonical_url unless seo_canonical_url.nil?
47
63
  hash['seo_noindex'] = seo_noindex unless seo_noindex.nil?
64
+ hash['password'] = password unless password.nil?
65
+ hash['max_clicks'] = max_clicks unless max_clicks.nil?
66
+ hash['track_conversions'] = track_conversions unless track_conversions.nil?
67
+ hash['routing_rules'] = routing_rules.map { |r| InputSerialization.rule_hash(r) } unless routing_rules.nil?
68
+ hash['ab_variants'] = ab_variants.map { |v| InputSerialization.variant_hash(v) } unless ab_variants.nil?
48
69
  hash
49
70
  end
50
71
  end
@@ -0,0 +1,27 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Rerout
4
+ # Request body for `POST /v1/projects/me/tags`. `name` is required; `color`
5
+ # is optional and omitted from the payload when not set (the server validates
6
+ # it against its palette and defaults to `"teal"`).
7
+ class CreateTagInput
8
+ attr_reader :name, :color
9
+
10
+ # @param name [String] required, the tag label.
11
+ # @param color [String, nil] optional tag color. Server default: `"teal"`.
12
+ def initialize(name:, color: nil)
13
+ raise ArgumentError, 'name is required' if name.nil? || name.to_s.empty?
14
+
15
+ @name = name
16
+ @color = color
17
+ freeze
18
+ end
19
+
20
+ # Serialize for the wire. `color` is only included when set.
21
+ def to_h
22
+ hash = { 'name' => name }
23
+ hash['color'] = color unless color.nil?
24
+ hash
25
+ end
26
+ end
27
+ end
@@ -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
@@ -0,0 +1,39 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Rerout
4
+ # Internal helpers for serializing Smart Link sub-objects (routing rules and
5
+ # A/B variants) accepted by {CreateLinkInput} / {UpdateLinkInput}. Each input
6
+ # element may be a {Rerout::Models::RoutingRule} / {Rerout::Models::AbVariant}
7
+ # value object or a plain Hash; both are coerced to the wire shape.
8
+ #
9
+ # @api private
10
+ module InputSerialization
11
+ module_function
12
+
13
+ # @param rule [Rerout::Models::RoutingRule, Hash]
14
+ # @return [Hash] string-keyed routing-rule payload.
15
+ def rule_hash(rule)
16
+ return rule.to_h if rule.is_a?(Models::RoutingRule)
17
+ return stringify(rule) if rule.is_a?(Hash)
18
+
19
+ raise ArgumentError, 'routing_rules entries must be Rerout::Models::RoutingRule or Hash'
20
+ end
21
+
22
+ # @param variant [Rerout::Models::AbVariant, Hash]
23
+ # @return [Hash] string-keyed A/B-variant payload (no server `id`).
24
+ def variant_hash(variant)
25
+ return variant.to_h if variant.is_a?(Models::AbVariant)
26
+ raise ArgumentError, 'ab_variants entries must be Rerout::Models::AbVariant or Hash' unless variant.is_a?(Hash)
27
+
28
+ h = stringify(variant)
29
+ h.delete('id')
30
+ h['weight'] = 1 unless h.key?('weight')
31
+ h
32
+ end
33
+
34
+ # Normalize a Hash to string keys without mutating the caller's object.
35
+ def stringify(hash)
36
+ hash.each_with_object({}) { |(k, v), out| out[k.to_s] = v }
37
+ end
38
+ end
39
+ end
data/lib/rerout/links.rb CHANGED
@@ -21,6 +21,30 @@ module Rerout
21
21
  Models::Link.from_hash(response)
22
22
  end
23
23
 
24
+ # Create many links in one call via `POST /v1/links/batch`.
25
+ #
26
+ # Each item may be a {Rerout::CreateLinkInput} or a plain Hash. Only the
27
+ # batch-supported fields (`target_url`, `code`, `expires_at`,
28
+ # `domain_hostname`) are forwarded — richer Smart Link fields are not
29
+ # accepted by the batch endpoint. A failed item does not raise; inspect
30
+ # `result.results[i].ok`.
31
+ #
32
+ # @param inputs [Array<Rerout::CreateLinkInput, Hash>]
33
+ # @return [Rerout::Models::BatchCreateLinksResult]
34
+ def create_batch(inputs)
35
+ links = Array(inputs).map { |item| batch_link_hash(item) }
36
+ if links.empty?
37
+ raise Error.new(
38
+ code: 'bad_request',
39
+ message: 'create_batch requires at least one link.',
40
+ status: 0
41
+ )
42
+ end
43
+
44
+ response = @client.request(method: :post, path: '/v1/links/batch', body: { 'links' => links })
45
+ Models::BatchCreateLinksResult.from_hash(response)
46
+ end
47
+
24
48
  # Paginated list of links.
25
49
  #
26
50
  # @param cursor [Integer, nil]
@@ -100,6 +124,28 @@ module Rerout
100
124
  raise ArgumentError, 'input must be a Rerout::CreateLinkInput or Hash'
101
125
  end
102
126
  end
127
+
128
+ # Reduce a create-link input to the fields the batch endpoint accepts:
129
+ # `target_url` (required), `code`, `expires_at`, `domain_hostname`.
130
+ def batch_link_hash(item)
131
+ source =
132
+ case item
133
+ when CreateLinkInput then item.to_h
134
+ when Hash then InputSerialization.stringify(item)
135
+ else
136
+ raise ArgumentError, 'batch link items must be a Rerout::CreateLinkInput or Hash'
137
+ end
138
+
139
+ unless source.key?('target_url') && !source['target_url'].to_s.empty?
140
+ raise ArgumentError, 'each batch link requires a target_url'
141
+ end
142
+
143
+ out = { 'target_url' => source['target_url'] }
144
+ %w[code expires_at domain_hostname].each do |key|
145
+ out[key] = source[key] unless source[key].nil?
146
+ end
147
+ out
148
+ end
103
149
  end
104
150
  end
105
151
  end
data/lib/rerout/models.rb CHANGED
@@ -35,12 +35,158 @@ module Rerout
35
35
  end
36
36
  end
37
37
 
38
+ # A tag with the number of live (non-deleted) links it is attached to —
39
+ # `{ id:, name:, color:, link_count: }`. Returned by `tags.list`; the
40
+ # create/update responses use the plain {Tag} (no `link_count`).
41
+ class TagSummary
42
+ attr_reader :id, :name, :color, :link_count
43
+
44
+ def initialize(id:, name:, color:, link_count:)
45
+ @id = id
46
+ @name = name
47
+ @color = color
48
+ @link_count = link_count
49
+ freeze
50
+ end
51
+
52
+ def self.from_hash(hash)
53
+ new(
54
+ id: hash['id'],
55
+ name: hash['name'],
56
+ color: hash['color'],
57
+ link_count: hash.fetch('link_count', 0)
58
+ )
59
+ end
60
+
61
+ def to_h
62
+ { id: id, name: name, color: color, link_count: link_count }
63
+ end
64
+
65
+ def ==(other)
66
+ other.is_a?(TagSummary) && other.to_h == to_h
67
+ end
68
+ alias eql? ==
69
+
70
+ def hash
71
+ [self.class, id, name, color, link_count].hash
72
+ end
73
+ end
74
+
75
+ # Result of `tags.list` — the project's tags with their live link counts.
76
+ class ListTagsResult
77
+ attr_reader :tags
78
+
79
+ def initialize(tags:)
80
+ @tags = tags.freeze
81
+ freeze
82
+ end
83
+
84
+ def self.from_hash(hash)
85
+ new(tags: (hash['tags'] || []).map { |t| TagSummary.from_hash(t) })
86
+ end
87
+
88
+ def to_h
89
+ { tags: tags.map(&:to_h) }
90
+ end
91
+
92
+ def ==(other)
93
+ other.is_a?(ListTagsResult) && other.to_h == to_h
94
+ end
95
+ alias eql? ==
96
+
97
+ def hash
98
+ to_h.hash
99
+ end
100
+ end
101
+
102
+ # A Smart Link routing rule — send matching visitors to `target_url`.
103
+ # `condition_type` is `"country"` or `"device"`; `condition_op` is `"is"`,
104
+ # `"is_not"`, or `"in"`; `condition_value` is the value to compare against
105
+ # (e.g. `"US"` or `"US,CA,GB"` for `"in"`).
106
+ class RoutingRule
107
+ attr_reader :condition_type, :condition_op, :condition_value, :target_url
108
+
109
+ def initialize(condition_type:, condition_op:, condition_value:, target_url:)
110
+ @condition_type = condition_type
111
+ @condition_op = condition_op
112
+ @condition_value = condition_value
113
+ @target_url = target_url
114
+ freeze
115
+ end
116
+
117
+ def self.from_hash(hash)
118
+ new(
119
+ condition_type: hash['condition_type'],
120
+ condition_op: hash['condition_op'],
121
+ condition_value: hash['condition_value'],
122
+ target_url: hash['target_url']
123
+ )
124
+ end
125
+
126
+ def to_h
127
+ {
128
+ 'condition_type' => condition_type,
129
+ 'condition_op' => condition_op,
130
+ 'condition_value' => condition_value,
131
+ 'target_url' => target_url
132
+ }
133
+ end
134
+
135
+ def ==(other)
136
+ other.is_a?(RoutingRule) && other.to_h == to_h
137
+ end
138
+ alias eql? ==
139
+
140
+ def hash
141
+ to_h.hash
142
+ end
143
+ end
144
+
145
+ # One A/B-test variant attached to a Smart Link. `id` is assigned
146
+ # server-side (read-only); `weight` controls the relative traffic share and
147
+ # defaults to `1`.
148
+ class AbVariant
149
+ attr_reader :id, :target_url, :weight
150
+
151
+ def initialize(target_url:, weight: 1, id: nil)
152
+ @id = id
153
+ @target_url = target_url
154
+ @weight = weight
155
+ freeze
156
+ end
157
+
158
+ def self.from_hash(hash)
159
+ new(
160
+ id: hash['id'],
161
+ target_url: hash['target_url'],
162
+ weight: hash.fetch('weight', 1)
163
+ )
164
+ end
165
+
166
+ # Render for create/update. The server-assigned `id` is never sent.
167
+ def to_h
168
+ { 'target_url' => target_url, 'weight' => weight }
169
+ end
170
+
171
+ def ==(other)
172
+ other.is_a?(AbVariant) && other.id == id &&
173
+ other.target_url == target_url && other.weight == weight
174
+ end
175
+ alias eql? ==
176
+
177
+ def hash
178
+ [self.class, id, target_url, weight].hash
179
+ end
180
+ end
181
+
38
182
  # A short link.
39
183
  class Link
40
184
  ATTRS = %i[
41
185
  code short_url domain_hostname target_url project_id expires_at
42
186
  is_active seo_title seo_description seo_image_url seo_canonical_url
43
187
  seo_noindex seo_updated_at tags created_at updated_at
188
+ password_protected max_clicks click_count track_conversions
189
+ routing_rules ab_variants
44
190
  ].freeze
45
191
 
46
192
  attr_reader(*ATTRS)
@@ -48,6 +194,8 @@ module Rerout
48
194
  def initialize(**attrs)
49
195
  ATTRS.each { |k| instance_variable_set(:"@#{k}", attrs[k]) }
50
196
  @tags = (@tags || []).freeze
197
+ @routing_rules = (@routing_rules || []).freeze
198
+ @ab_variants = (@ab_variants || []).freeze
51
199
  freeze
52
200
  end
53
201
 
@@ -68,13 +216,24 @@ module Rerout
68
216
  seo_updated_at: hash['seo_updated_at'],
69
217
  tags: (hash['tags'] || []).map { |t| Tag.from_hash(t) },
70
218
  created_at: hash['created_at'],
71
- updated_at: hash['updated_at']
219
+ updated_at: hash['updated_at'],
220
+ password_protected: hash.fetch('password_protected', false),
221
+ max_clicks: hash['max_clicks'],
222
+ click_count: hash.fetch('click_count', 0),
223
+ track_conversions: hash.fetch('track_conversions', false),
224
+ routing_rules: (hash['routing_rules'] || []).map { |r| RoutingRule.from_hash(r) },
225
+ ab_variants: (hash['ab_variants'] || []).map { |v| AbVariant.from_hash(v) }
72
226
  )
73
227
  end
74
228
 
75
229
  def to_h
76
230
  ATTRS.to_h do |k|
77
- [k, k == :tags ? tags.map(&:to_h) : public_send(k)]
231
+ [k, case k
232
+ when :tags then tags.map(&:to_h)
233
+ when :routing_rules then routing_rules.map(&:to_h)
234
+ when :ab_variants then ab_variants.map(&:to_h)
235
+ else public_send(k)
236
+ end]
78
237
  end
79
238
  end
80
239
 
@@ -301,5 +460,218 @@ module Rerout
301
460
  [self.class, id, name, slug].hash
302
461
  end
303
462
  end
463
+
464
+ # A webhook endpoint registered to the project. Mirrors the server-side
465
+ # `WebhookEndpointResponse`.
466
+ class Webhook
467
+ ATTRS = %i[
468
+ id project_id name url events is_active payload_format
469
+ created_at updated_at last_delivery_at last_success_at last_failure_at
470
+ ].freeze
471
+
472
+ attr_reader(*ATTRS)
473
+
474
+ def initialize(**attrs)
475
+ ATTRS.each { |k| instance_variable_set(:"@#{k}", attrs[k]) }
476
+ @events = (@events || []).freeze
477
+ freeze
478
+ end
479
+
480
+ def self.from_hash(hash)
481
+ new(
482
+ id: hash['id'],
483
+ project_id: hash['project_id'],
484
+ name: hash['name'],
485
+ url: hash['url'],
486
+ events: hash['events'] || [],
487
+ is_active: hash['is_active'],
488
+ payload_format: hash['payload_format'],
489
+ created_at: hash['created_at'],
490
+ updated_at: hash['updated_at'],
491
+ last_delivery_at: hash['last_delivery_at'],
492
+ last_success_at: hash['last_success_at'],
493
+ last_failure_at: hash['last_failure_at']
494
+ )
495
+ end
496
+
497
+ def to_h
498
+ ATTRS.to_h { |k| [k, public_send(k)] }
499
+ end
500
+
501
+ def ==(other)
502
+ other.is_a?(Webhook) && other.to_h == to_h
503
+ end
504
+ alias eql? ==
505
+
506
+ def hash
507
+ to_h.hash
508
+ end
509
+ end
510
+
511
+ # Result of creating a webhook. The `signing_secret` (`whsec_…`) is
512
+ # returned **once** — store it now; it is never shown again.
513
+ class CreatedWebhook
514
+ attr_reader :endpoint, :signing_secret
515
+
516
+ def initialize(endpoint:, signing_secret:)
517
+ @endpoint = endpoint
518
+ @signing_secret = signing_secret
519
+ freeze
520
+ end
521
+
522
+ def self.from_hash(hash)
523
+ new(
524
+ endpoint: Webhook.from_hash(hash['endpoint'] || {}),
525
+ signing_secret: hash['signing_secret']
526
+ )
527
+ end
528
+
529
+ def to_h
530
+ { endpoint: endpoint.to_h, signing_secret: signing_secret }
531
+ end
532
+
533
+ def ==(other)
534
+ other.is_a?(CreatedWebhook) && other.to_h == to_h
535
+ end
536
+ alias eql? ==
537
+
538
+ def hash
539
+ to_h.hash
540
+ end
541
+ end
542
+
543
+ # List of webhook endpoints plus every event type the server can deliver.
544
+ class ListWebhooksResult
545
+ attr_reader :endpoints, :event_types
546
+
547
+ def initialize(endpoints:, event_types:)
548
+ @endpoints = endpoints.freeze
549
+ @event_types = event_types.freeze
550
+ freeze
551
+ end
552
+
553
+ def self.from_hash(hash)
554
+ new(
555
+ endpoints: (hash['endpoints'] || []).map { |e| Webhook.from_hash(e) },
556
+ event_types: hash['event_types'] || []
557
+ )
558
+ end
559
+
560
+ def to_h
561
+ { endpoints: endpoints.map(&:to_h), event_types: event_types }
562
+ end
563
+
564
+ def ==(other)
565
+ other.is_a?(ListWebhooksResult) && other.to_h == to_h
566
+ end
567
+ alias eql? ==
568
+
569
+ def hash
570
+ to_h.hash
571
+ end
572
+ end
573
+
574
+ # Result of recording a conversion via `POST /v1/conversions`. `recorded`
575
+ # is true when stored; `duplicate` is true when an identical conversion for
576
+ # the same click had already been recorded (the call is idempotent).
577
+ class RecordedConversion
578
+ attr_reader :recorded, :duplicate
579
+
580
+ def initialize(recorded:, duplicate:)
581
+ @recorded = recorded
582
+ @duplicate = duplicate
583
+ freeze
584
+ end
585
+
586
+ def self.from_hash(hash)
587
+ new(
588
+ recorded: hash.fetch('recorded', false),
589
+ duplicate: hash.fetch('duplicate', false)
590
+ )
591
+ end
592
+
593
+ def to_h
594
+ { recorded: recorded, duplicate: duplicate }
595
+ end
596
+
597
+ def ==(other)
598
+ other.is_a?(RecordedConversion) && other.recorded == recorded && other.duplicate == duplicate
599
+ end
600
+ alias eql? ==
601
+
602
+ def hash
603
+ [self.class, recorded, duplicate].hash
604
+ end
605
+ end
606
+
607
+ # Outcome of one link in a `links.create_batch` call. `index` is the input
608
+ # position; `ok` is true on success (then `code` is set), false on failure
609
+ # (then `error` carries the reason).
610
+ class BatchLinkResult
611
+ attr_reader :index, :ok, :code, :error
612
+
613
+ def initialize(index:, ok:, code: nil, error: nil)
614
+ @index = index
615
+ @ok = ok
616
+ @code = code
617
+ @error = error
618
+ freeze
619
+ end
620
+
621
+ def self.from_hash(hash)
622
+ new(
623
+ index: hash['index'],
624
+ ok: hash['ok'],
625
+ code: hash['code'],
626
+ error: hash['error']
627
+ )
628
+ end
629
+
630
+ def to_h
631
+ { index: index, ok: ok, code: code, error: error }
632
+ end
633
+
634
+ def ==(other)
635
+ other.is_a?(BatchLinkResult) && other.to_h == to_h
636
+ end
637
+ alias eql? ==
638
+
639
+ def hash
640
+ to_h.hash
641
+ end
642
+ end
643
+
644
+ # Aggregate result of `links.create_batch` (`POST /v1/links/batch`).
645
+ class BatchCreateLinksResult
646
+ attr_reader :created, :total, :results
647
+
648
+ def initialize(created:, total:, results:)
649
+ @created = created
650
+ @total = total
651
+ @results = results.freeze
652
+ freeze
653
+ end
654
+
655
+ def self.from_hash(hash)
656
+ new(
657
+ created: hash.fetch('created', 0),
658
+ total: hash.fetch('total', 0),
659
+ results: (hash['results'] || []).map { |r| BatchLinkResult.from_hash(r) }
660
+ )
661
+ end
662
+
663
+ def to_h
664
+ { created: created, total: total, results: results.map(&:to_h) }
665
+ end
666
+
667
+ def ==(other)
668
+ other.is_a?(BatchCreateLinksResult) && other.to_h == to_h
669
+ end
670
+ alias eql? ==
671
+
672
+ def hash
673
+ to_h.hash
674
+ end
675
+ end
304
676
  end
305
677
  end
@@ -0,0 +1,82 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'erb'
4
+
5
+ module Rerout
6
+ module Resources
7
+ # Tag management namespace — list, create, update, and delete the tags that
8
+ # can be attached to links for the project that owns the API key. Reach it
9
+ # via `client.tags`.
10
+ class Tags
11
+ # @param client [Rerout::Client]
12
+ def initialize(client)
13
+ @client = client
14
+ end
15
+
16
+ # List the project's tags with their live link counts.
17
+ #
18
+ # @return [Rerout::Models::ListTagsResult]
19
+ def list
20
+ response = @client.request(method: :get, path: '/v1/projects/me/tags')
21
+ Models::ListTagsResult.from_hash(response)
22
+ end
23
+
24
+ # Create a tag. `color` is optional; the server validates and defaults it.
25
+ #
26
+ # @param input [Rerout::CreateTagInput, Hash] the request body.
27
+ # @return [Rerout::Models::Tag] the created tag (no `link_count`).
28
+ def create(input)
29
+ body = coerce_create_input(input)
30
+ response = @client.request(method: :post, path: '/v1/projects/me/tags', body: body)
31
+ Models::Tag.from_hash(response)
32
+ end
33
+
34
+ # Update a tag's name and/or color. Mirrors `links.update`: omitted fields
35
+ # are left unchanged. There is no client-side empty-payload check — the
36
+ # server rejects a fully empty patch with `400`.
37
+ #
38
+ # @param tag_id [String] the tag id (`tag_…`).
39
+ # @param input [Rerout::UpdateTagInput, Hash] the patch body.
40
+ # @return [Rerout::Models::Tag] the updated tag.
41
+ def update(tag_id, input)
42
+ body = coerce_update_input(input)
43
+ response = @client.request(method: :patch, path: tag_path(tag_id), body: body)
44
+ Models::Tag.from_hash(response)
45
+ end
46
+
47
+ # Delete a tag and drop its assignments from all links. Idempotent.
48
+ #
49
+ # @param tag_id [String] the tag id (`tag_…`).
50
+ # @return [Hash] `{ "deleted" => true }`
51
+ def delete(tag_id)
52
+ @client.request(method: :delete, path: tag_path(tag_id))
53
+ end
54
+
55
+ private
56
+
57
+ def tag_path(tag_id)
58
+ raise ArgumentError, 'tag_id is required' if tag_id.nil? || tag_id.to_s.empty?
59
+
60
+ "/v1/projects/me/tags/#{ERB::Util.url_encode(tag_id.to_s)}"
61
+ end
62
+
63
+ def coerce_create_input(input)
64
+ case input
65
+ when CreateTagInput then input.to_h
66
+ when Hash then input
67
+ else
68
+ raise ArgumentError, 'input must be a Rerout::CreateTagInput or Hash'
69
+ end
70
+ end
71
+
72
+ def coerce_update_input(input)
73
+ case input
74
+ when UpdateTagInput then input.to_h
75
+ when Hash then input
76
+ else
77
+ raise ArgumentError, 'input must be a Rerout::UpdateTagInput or Hash'
78
+ end
79
+ end
80
+ end
81
+ end
82
+ end
@@ -37,11 +37,18 @@ module Rerout
37
37
  FIELDS = %i[
38
38
  target_url expires_at is_active seo_title seo_description
39
39
  seo_image_url seo_canonical_url seo_noindex
40
+ password max_clicks track_conversions routing_rules ab_variants
40
41
  ].freeze
41
42
 
43
+ # Smart Link fields that are a full replacement (an Array of value objects
44
+ # or Hashes) rather than a scalar — serialized element-by-element.
45
+ LIST_FIELDS = %i[routing_rules ab_variants].freeze
46
+
42
47
  def initialize(target_url: OMIT, expires_at: OMIT, is_active: OMIT,
43
48
  seo_title: OMIT, seo_description: OMIT, seo_image_url: OMIT,
44
- seo_canonical_url: OMIT, seo_noindex: OMIT)
49
+ seo_canonical_url: OMIT, seo_noindex: OMIT,
50
+ password: OMIT, max_clicks: OMIT, track_conversions: OMIT,
51
+ routing_rules: OMIT, ab_variants: OMIT)
45
52
  @values = {
46
53
  target_url: target_url,
47
54
  expires_at: expires_at,
@@ -50,20 +57,26 @@ module Rerout
50
57
  seo_description: seo_description,
51
58
  seo_image_url: seo_image_url,
52
59
  seo_canonical_url: seo_canonical_url,
53
- seo_noindex: seo_noindex
60
+ seo_noindex: seo_noindex,
61
+ password: password,
62
+ max_clicks: max_clicks,
63
+ track_conversions: track_conversions,
64
+ routing_rules: routing_rules,
65
+ ab_variants: ab_variants
54
66
  }
55
67
  freeze
56
68
  end
57
69
 
58
70
  # Serialize for the wire. Unset fields are omitted; `Rerout::CLEAR`
59
- # becomes `null`.
71
+ # becomes `null`. `routing_rules` / `ab_variants` are a full replacement
72
+ # and serialize their elements (value objects or Hashes).
60
73
  def to_h
61
74
  out = {}
62
75
  FIELDS.each do |field|
63
76
  v = @values[field]
64
77
  next if v.equal?(OMIT)
65
78
 
66
- out[field.to_s] = v.equal?(CLEAR) ? nil : v
79
+ out[field.to_s] = serialize_field(field, v)
67
80
  end
68
81
  out
69
82
  end
@@ -77,5 +90,17 @@ module Rerout
77
90
  def value_for(field)
78
91
  @values.fetch(field)
79
92
  end
93
+
94
+ private
95
+
96
+ def serialize_field(field, value)
97
+ return nil if value.equal?(CLEAR)
98
+
99
+ case field
100
+ when :routing_rules then value.map { |r| InputSerialization.rule_hash(r) }
101
+ when :ab_variants then value.map { |v| InputSerialization.variant_hash(v) }
102
+ else value
103
+ end
104
+ end
80
105
  end
81
106
  end
@@ -0,0 +1,37 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Rerout
4
+ # Request body for `PATCH /v1/projects/me/tags/:tag_id`. Both fields are
5
+ # optional; an omitted field is left unchanged. Unlike {UpdateLinkInput},
6
+ # there is no client-side empty-payload check — mirroring the reference SDKs,
7
+ # the server returns `400` for a fully empty patch. Neither field is nullable,
8
+ # so there is no `Rerout::CLEAR` handling here.
9
+ #
10
+ # @example
11
+ # Rerout::UpdateTagInput.new(name: 'Renamed')
12
+ # Rerout::UpdateTagInput.new(color: 'red')
13
+ class UpdateTagInput
14
+ attr_reader :name, :color
15
+
16
+ # @param name [String, nil] new label. Omitted when not set.
17
+ # @param color [String, nil] new color. Omitted when not set.
18
+ def initialize(name: OMIT, color: OMIT)
19
+ @name = name
20
+ @color = color
21
+ freeze
22
+ end
23
+
24
+ # Serialize for the wire. Only fields that were set are included.
25
+ def to_h
26
+ hash = {}
27
+ hash['name'] = name unless name.equal?(OMIT)
28
+ hash['color'] = color unless color.equal?(OMIT)
29
+ hash
30
+ end
31
+
32
+ # @return [Boolean] true when no field was set.
33
+ def empty?
34
+ to_h.empty?
35
+ end
36
+ end
37
+ end
@@ -2,5 +2,5 @@
2
2
 
3
3
  module Rerout
4
4
  # Library version. Follows semantic versioning.
5
- VERSION = '0.2.0'
5
+ VERSION = '0.5.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
@@ -3,13 +3,20 @@
3
3
  require_relative 'rerout/version'
4
4
  require_relative 'rerout/error'
5
5
  require_relative 'rerout/models'
6
+ require_relative 'rerout/input_serialization'
6
7
  require_relative 'rerout/create_link_input'
7
8
  require_relative 'rerout/update_link_input'
9
+ require_relative 'rerout/create_webhook_input'
10
+ require_relative 'rerout/create_tag_input'
11
+ require_relative 'rerout/update_tag_input'
8
12
  require_relative 'rerout/qr_options'
9
13
  require_relative 'rerout/webhooks'
10
14
  require_relative 'rerout/links'
11
15
  require_relative 'rerout/project'
12
16
  require_relative 'rerout/qr'
17
+ require_relative 'rerout/webhooks_resource'
18
+ require_relative 'rerout/conversions_resource'
19
+ require_relative 'rerout/tags_resource'
13
20
  require_relative 'rerout/client'
14
21
 
15
22
  # Official Ruby SDK for the Rerout branded-link API.
metadata CHANGED
@@ -1,14 +1,13 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: rerout
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.2.0
4
+ version: 0.5.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Codecraft Solutions
8
- autorequire:
9
8
  bindir: bin
10
9
  cert_chain: []
11
- date: 2026-06-02 00:00:00.000000000 Z
10
+ date: 1980-01-02 00:00:00.000000000 Z
12
11
  dependencies:
13
12
  - !ruby/object:Gem::Dependency
14
13
  name: faraday
@@ -109,16 +108,23 @@ files:
109
108
  - README.md
110
109
  - lib/rerout.rb
111
110
  - lib/rerout/client.rb
111
+ - lib/rerout/conversions_resource.rb
112
112
  - lib/rerout/create_link_input.rb
113
+ - lib/rerout/create_tag_input.rb
114
+ - lib/rerout/create_webhook_input.rb
113
115
  - lib/rerout/error.rb
116
+ - lib/rerout/input_serialization.rb
114
117
  - lib/rerout/links.rb
115
118
  - lib/rerout/models.rb
116
119
  - lib/rerout/project.rb
117
120
  - lib/rerout/qr.rb
118
121
  - lib/rerout/qr_options.rb
122
+ - lib/rerout/tags_resource.rb
119
123
  - lib/rerout/update_link_input.rb
124
+ - lib/rerout/update_tag_input.rb
120
125
  - lib/rerout/version.rb
121
126
  - lib/rerout/webhooks.rb
127
+ - lib/rerout/webhooks_resource.rb
122
128
  homepage: https://github.com/ModestNerds-Co/rerout-sdks
123
129
  licenses:
124
130
  - MIT
@@ -129,7 +135,6 @@ metadata:
129
135
  bug_tracker_uri: https://github.com/ModestNerds-Co/rerout-sdks/issues
130
136
  documentation_uri: https://rerout.co/docs
131
137
  rubygems_mfa_required: 'true'
132
- post_install_message:
133
138
  rdoc_options: []
134
139
  require_paths:
135
140
  - lib
@@ -144,8 +149,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
144
149
  - !ruby/object:Gem::Version
145
150
  version: '0'
146
151
  requirements: []
147
- rubygems_version: 3.0.3.1
148
- signing_key:
152
+ rubygems_version: 3.6.9
149
153
  specification_version: 4
150
154
  summary: Official Ruby SDK for the Rerout branded-link API.
151
155
  test_files: []