sink-cool 0.1.2 → 0.2.1

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: 1b80f9d22b726100b1e3e5b1ee1dd8db7e4de9bd154a33cfb42c2797333d1b8b
4
- data.tar.gz: b7f8f0dab846a2f53bc5aa5704a6408c7a7c496276c9a60a1335fbb127e2829e
3
+ metadata.gz: 117661123578a6b91926715b23db4f780820b7cfe1f420e79a1afc37c962a01b
4
+ data.tar.gz: eb5bc2317aa7d9b6d973d811d0ed5ca938ddee0f6c445aadfed53fcfb8b34380
5
5
  SHA512:
6
- metadata.gz: 50c9749e0271c7d9dcc870f515e773b352355db3e347b7e7d5a609cd21a36525f2a2ef86c57686ca4883dc69da845a2c6cd110da2bf219ca61d87a20bd91a023
7
- data.tar.gz: e364f3d9217a4bbeeced34575d509b0699ffbbba93150fb0a15f6f989094c4a10ba4a038ff4ec99b0c4483878c3e03d0b3cf9ad218a757c4618bf34956a2c3a8
6
+ metadata.gz: ff112350ba6829bc44186218dd35cedf7f67e71fd8ae9ec3dd41213a56b35b038129e879f0f7f3230740629f42cc6725e003a7acfd71c71c89ee3f3408505cf0
7
+ data.tar.gz: c7d2cebbe780ea928b9e022caa8d24644f3b6525f063e24443d50ecf99e89a8a3e1f29a41b06dc4fe91b67b90224708c1fead5855e4f52f298c8fba582696a91
data/CHANGELOG.md CHANGED
@@ -4,6 +4,39 @@ All notable changes to this project will be documented in this file.
4
4
 
5
5
  ## Unreleased
6
6
 
7
+ ## 0.2.1 - 2026-07-25
8
+
9
+ ### Added
10
+
11
+ - Derive `Link#short_link` from `base_url` and the slug on the read endpoints, which the API only returns for writes. A `shortLink` in the response still wins.
12
+
13
+ ## 0.2.0 - 2026-07-25
14
+
15
+ ### Added
16
+
17
+ - `Sink::Link` value objects with snake_case readers, `Time` timestamps, predicate methods, `to_h`, and `[]` access to fields without a reader.
18
+ - `Sink::Page` collections including `Enumerable`, exposing `cursor`, `complete?`, and `more?`.
19
+ - `Sink::Tag` records with `name` and `count`.
20
+ - `Client#each_link` to iterate every page without handling cursors manually.
21
+ - Error subclasses for each API failure mode: `Sink::ValidationError`, `Unauthorized`, `Forbidden`, `NotFound`, `Conflict`, `StorageNotReady`, `RateLimited`, `ServerError`, `ConnectionError`, and `TimeoutError`.
22
+ - `Sink::Error#data` for the validation details returned by the API.
23
+
24
+ ### Changed
25
+
26
+ - **Breaking:** link endpoints return `Sink::Link` and `Sink::Page` objects instead of raw hashes and arrays. Call `to_h` for the previous key names in snake_case.
27
+ - **Breaking:** link attributes are passed as snake_case keyword arguments and camelized for the API, so `redirectWithQuery` becomes `redirect_with_query`. Unknown attributes now raise `ArgumentError`.
28
+ - **Breaking:** `create_link`, `edit_link`, and `upsert_link` take keyword arguments, with `url` required by all three and `slug` also required by `edit_link`.
29
+ - **Breaking:** `delete_link` returns `true` instead of `nil`.
30
+ - **Breaking:** `verify` returns a hash with snake_case symbol keys.
31
+ - `expires_at` accepts a `Time`, a `Date`, or a unix timestamp and is sent as `expiration`.
32
+ - Timeouts, TLS, and socket failures raise `Sink::Error` subclasses instead of leaking `Net::HTTP` exceptions.
33
+
34
+ ### Fixed
35
+
36
+ - Skip blank `message` values when building error messages, so the Sink API `statusMessage` is no longer replaced by an empty string.
37
+ - Read `statusText` as an error message source, matching newer Sink releases.
38
+ - Validate the token before the base URL so an empty token reports the right argument.
39
+
7
40
  ## 0.1.2 - 2026-07-23
8
41
 
9
42
  ### Fixed
data/README.md CHANGED
@@ -1,5 +1,9 @@
1
1
  # sink-cool
2
2
 
3
+ [![CI](https://github.com/7a6163/sink-cool/actions/workflows/ci.yml/badge.svg)](https://github.com/7a6163/sink-cool/actions/workflows/ci.yml)
4
+ [![Gem Version](https://img.shields.io/gem/v/sink-cool.svg)](https://rubygems.org/gems/sink-cool)
5
+ [![codecov](https://codecov.io/gh/7a6163/sink-cool/branch/main/graph/badge.svg)](https://codecov.io/gh/7a6163/sink-cool)
6
+
3
7
  Ruby client for the [Sink](https://sink.cool/) URL shortener API.
4
8
 
5
9
  Requires Ruby 3.3 or newer.
@@ -59,19 +63,17 @@ client = Sink::Client.new(
59
63
  client = Sink.client
60
64
 
61
65
  client.verify
66
+ # => { name: "Sink", auth_method: "site-token", user_email: "admin@example.com", ... }
62
67
 
63
68
  client.create_link(
64
69
  url: "https://example.com/articles/1",
65
70
  slug: "article-1",
66
- tags: ["articles"]
67
- )
68
-
69
- client.edit_link(
70
- url: "https://example.com/articles/2",
71
- slug: "article-1",
72
- tags: ["articles"]
71
+ tags: ["articles"],
72
+ redirect_with_query: true,
73
+ expires_at: 1.week.from_now
73
74
  )
74
75
 
76
+ client.edit_link(url: "https://example.com/articles/2", slug: "article-1")
75
77
  client.upsert_link(url: "https://example.com", slug: "example")
76
78
  client.link("example")
77
79
  client.links(limit: 20, sort: "newest", status: "active")
@@ -81,20 +83,111 @@ client.check_links(limit: 6, timeout: 6)
81
83
  client.delete_link("example")
82
84
  ```
83
85
 
84
- API responses are returned as Ruby hashes or arrays. Requests returning HTTP
85
- 204 return `nil`.
86
+ Attributes are snake_case and converted to the camelCase names the API expects,
87
+ so `redirect_with_query` is sent as `redirectWithQuery`. Unknown attributes
88
+ raise `ArgumentError` instead of being silently dropped by the server.
89
+ `expires_at` accepts a `Time`, a `Date`, or a unix timestamp.
90
+
91
+ ### Links
92
+
93
+ `create_link`, `edit_link`, `upsert_link`, and `link` all return a `Sink::Link`:
94
+
95
+ ```ruby
96
+ link = client.create_link(url: "https://example.com", slug: "example")
97
+
98
+ link.slug # => "example"
99
+ link.short_link # => "https://sink.example.com/example"
100
+ link.created_at # => 2026-07-24 12:00:00 UTC
101
+ link.expires_at # => Time or nil
102
+ link.expired? # => false
103
+ link.cloaking? # => false
104
+ link.redirect_with_query? # => true
105
+ link.tags # => ["articles"]
106
+ link.geo # => { "US" => "https://example.com/us" }
107
+ link.to_h # => { slug: "example", url: "https://example.com", ... }
108
+ link[:any_future_field] # raw access to fields without a reader
109
+ ```
110
+
111
+ The API only returns `shortLink` for the write endpoints, where it builds the
112
+ value from the protocol and host of the request. `short_link` is therefore
113
+ derived from `base_url` and the slug for `link`, `links`, and `search_links`,
114
+ which matches the API as long as `base_url` is the Worker domain that serves the
115
+ short links.
116
+
117
+ `upsert_link` never overwrites an existing slug, so check which happened:
118
+
119
+ ```ruby
120
+ link = client.upsert_link(url: "https://example.com", slug: "example")
121
+
122
+ link.created? # => true when the link was stored
123
+ link.existing? # => true when the slug already pointed somewhere else
124
+ ```
125
+
126
+ `delete_link` returns `true`.
127
+
128
+ ### Collections
129
+
130
+ `links`, `search_links`, and `check_links` return a `Sink::Page`, which is
131
+ `Enumerable`:
132
+
133
+ ```ruby
134
+ page = client.links(limit: 20)
135
+
136
+ page.map(&:slug)
137
+ page.cursor # => "..." pagination cursor
138
+ page.complete? # => false when more pages are available
139
+ page.more? # => true when a following page can be fetched
140
+ ```
141
+
142
+ Use `each_link` to walk every page without handling cursors:
143
+
144
+ ```ruby
145
+ client.each_link(tag: "articles") { |link| puts link.short_link }
146
+ client.each_link.lazy.select(&:expired?).first(10)
147
+ ```
148
+
149
+ `tags` returns `Sink::Tag` records with `name` and `count`.
86
150
 
87
151
  ## Errors
88
152
 
89
- Non-successful responses raise `Sink::Error`:
153
+ Non-successful responses raise a subclass of `Sink::Error`, so a Rails
154
+ controller can rescue the cases it cares about:
155
+
156
+ ```ruby
157
+ class LinksController < ApplicationController
158
+ rescue_from Sink::NotFound, with: :not_found
159
+
160
+ def create
161
+ link = Sink.client.create_link(url: params[:url], slug: params[:slug])
162
+ render json: link.to_h, status: :created
163
+ rescue Sink::Conflict
164
+ render json: { error: "slug already taken" }, status: :conflict
165
+ end
166
+ end
167
+ ```
168
+
169
+ | Class | Raised for |
170
+ | --- | --- |
171
+ | `Sink::ValidationError` | 400, 422 (`error.data` holds the API details) |
172
+ | `Sink::Unauthorized` | 401, usually a wrong site token |
173
+ | `Sink::Forbidden` | 403, including preview mode |
174
+ | `Sink::NotFound` | 404 |
175
+ | `Sink::Conflict` | 409, the slug already exists |
176
+ | `Sink::StorageNotReady` | 423, open Dashboard → Links once after deploying |
177
+ | `Sink::RateLimited` | 429 |
178
+ | `Sink::ServerError` | 5xx |
179
+ | `Sink::TimeoutError` | open or read timeout |
180
+ | `Sink::ConnectionError` | DNS, TLS, and socket failures |
181
+
182
+ Every error exposes `status`, `message`, `body`, and `data`:
90
183
 
91
184
  ```ruby
92
185
  begin
93
186
  Sink.client.link("missing")
94
187
  rescue Sink::Error => error
95
- error.status
96
- error.message
97
- error.body
188
+ error.status # => 404
189
+ error.message # => "Not Found"
190
+ error.body # => parsed response body
98
191
  end
99
192
  ```
100
193
 
data/lib/sink/client.rb CHANGED
@@ -2,6 +2,7 @@
2
2
 
3
3
  require "json"
4
4
  require "net/http"
5
+ require "openssl"
5
6
  require "uri"
6
7
 
7
8
  module Sink
@@ -12,48 +13,121 @@ module Sink
12
13
  put: Net::HTTP::Put
13
14
  }.freeze
14
15
 
16
+ LINK_ATTRIBUTES = %i[
17
+ url slug comment expiration expires_at title description image apple google
18
+ cloaking redirect_with_query password unsafe geo tags
19
+ ].freeze
20
+
21
+ TIMEOUT_ERRORS = [Net::OpenTimeout, Net::ReadTimeout, Net::WriteTimeout, Timeout::Error].freeze
22
+ CONNECTION_ERRORS = [
23
+ IOError, SocketError, SystemCallError, Net::ProtocolError, OpenSSL::SSL::SSLError
24
+ ].freeze
25
+
15
26
  def initialize(base_url:, token:, open_timeout: 5, read_timeout: 30)
16
- @base_url = validate_base_url(base_url)
17
27
  @token = token.to_s
28
+ raise ArgumentError, "token is required" if @token.empty?
29
+
30
+ @base_url = validate_base_url(base_url)
18
31
  @open_timeout = open_timeout
19
32
  @read_timeout = read_timeout
20
- raise ArgumentError, "token is required" if @token.empty?
21
33
  end
22
34
 
23
- def verify = request(:get, "/api/verify")
35
+ def verify = Keys.underscore_keys(request(:get, "/api/verify") || {})
24
36
 
25
- def create_link(attributes) = request(:post, "/api/link/create", body: attributes)
37
+ def create_link(url:, **attributes)
38
+ build_link(request(:post, "/api/link/create", body: link_payload(attributes.merge(url: url))))
39
+ end
26
40
 
27
- def edit_link(attributes) = request(:put, "/api/link/edit", body: attributes)
41
+ def edit_link(url:, slug:, **attributes)
42
+ build_link(request(:put, "/api/link/edit", body: link_payload(attributes.merge(url: url, slug: slug))))
43
+ end
28
44
 
29
- def upsert_link(attributes) = request(:post, "/api/link/upsert", body: attributes)
45
+ def upsert_link(url:, **attributes)
46
+ build_link(request(:post, "/api/link/upsert", body: link_payload(attributes.merge(url: url))))
47
+ end
30
48
 
31
- def delete_link(slug) = request(:post, "/api/link/delete", body: { slug: slug })
49
+ def delete_link(slug)
50
+ request(:post, "/api/link/delete", body: { "slug" => slug })
51
+ true
52
+ end
32
53
 
33
- def link(slug) = request(:get, "/api/link/query", query: { slug: slug })
54
+ def link(slug) = build_link(request(:get, "/api/link/query", query: { slug: slug }))
34
55
 
35
56
  def links(limit: nil, cursor: nil, sort: nil, tag: nil, status: nil)
36
- request(:get, "/api/link/list", query: {
57
+ body = request(:get, "/api/link/list", query: {
37
58
  limit: limit, cursor: cursor, sort: sort, tag: tag, status: status
38
59
  })
60
+
61
+ link_page(body)
62
+ end
63
+
64
+ def each_link(**options)
65
+ return enum_for(:each_link, **options) unless block_given?
66
+
67
+ cursor = nil
68
+ loop do
69
+ page = links(**options, cursor: cursor)
70
+ page.each { |link| yield link }
71
+ break unless page.more?
72
+
73
+ cursor = page.cursor
74
+ end
39
75
  end
40
76
 
41
77
  def search_links(query: nil, url: nil, limit: nil, tag: nil, status: nil)
42
- request(:get, "/api/link/search", query: {
78
+ body = request(:get, "/api/link/search", query: {
43
79
  q: query, url: url, limit: limit, tag: tag, status: status
44
80
  })
81
+
82
+ link_page(body)
45
83
  end
46
84
 
47
- def tags = request(:get, "/api/link/tags")
85
+ def tags
86
+ Array(request(:get, "/api/link/tags")).map { |tag| Tag.from_response(tag) }
87
+ end
48
88
 
49
89
  def check_links(cursor: nil, limit: 6, timeout: 6)
50
- request(:post, "/api/link/check", body: {
51
- cursor: cursor, limit: limit, timeout: timeout
90
+ body = request(:post, "/api/link/check", body: {
91
+ "cursor" => cursor, "limit" => limit, "timeout" => timeout
52
92
  }.compact)
93
+
94
+ page(body, "results") { |result| Keys.underscore_keys(result) }
53
95
  end
54
96
 
55
97
  private
56
98
 
99
+ def link_payload(attributes)
100
+ unknown = attributes.keys - LINK_ATTRIBUTES
101
+ raise ArgumentError, "unknown link attributes: #{unknown.join(", ")}" if unknown.any?
102
+
103
+ payload = attributes.compact
104
+ expires_at = payload.delete(:expires_at)
105
+ payload[:expiration] = unix_timestamp(expires_at) unless expires_at.nil?
106
+
107
+ Keys.camelize_keys(payload)
108
+ end
109
+
110
+ def unix_timestamp(value)
111
+ return value.to_i if value.is_a?(Numeric)
112
+ return value.to_time.to_i if value.respond_to?(:to_time)
113
+
114
+ raise ArgumentError, "expires_at must be a Time or a unix timestamp"
115
+ end
116
+
117
+ def build_link(body) = Link.from_response(body, base_url: @base_url)
118
+
119
+ def link_page(body) = page(body, "links") { |link| build_link(link) }
120
+
121
+ def page(body, key, &build)
122
+ records, cursor, complete = case body
123
+ when Array then [body, nil, true]
124
+ when Hash then [Array(body[key]), body["cursor"], body.fetch("list_complete", true)]
125
+ else [[], nil, true]
126
+ end
127
+
128
+ Page.new(records: records.map(&build), cursor: cursor, complete: complete)
129
+ end
130
+
57
131
  def request(method, path, query: nil, body: nil)
58
132
  uri = URI("#{@base_url}#{path}")
59
133
  params = query&.compact
@@ -69,7 +143,15 @@ module Sink
69
143
  http_request = REQUEST_CLASSES.fetch(method).new(uri, headers)
70
144
  http_request.body = JSON.generate(body) if body
71
145
 
72
- parse_response(perform_request(uri, http_request))
146
+ parse_response(dispatch(uri, http_request))
147
+ end
148
+
149
+ def dispatch(uri, http_request)
150
+ perform_request(uri, http_request)
151
+ rescue *TIMEOUT_ERRORS => error
152
+ raise TimeoutError.new("#{uri.host} timed out: #{error.message}")
153
+ rescue *CONNECTION_ERRORS => error
154
+ raise ConnectionError.new("#{uri.host} is unreachable: #{error.message}")
73
155
  end
74
156
 
75
157
  def perform_request(uri, request)
@@ -88,11 +170,16 @@ module Sink
88
170
  body = parse_body(response.body)
89
171
  return body if response.is_a?(Net::HTTPSuccess)
90
172
 
91
- message = if body.is_a?(Hash)
92
- body["message"] || body["statusMessage"]
93
- end
94
- message ||= "HTTP #{response.code} #{response.message}"
95
- raise Error.new(status: response.code.to_i, message: message, body: body)
173
+ message = error_message(body) || "HTTP #{response.code} #{response.message}"
174
+ raise Error.for(status: response.code.to_i, message: message, body: body)
175
+ end
176
+
177
+ def error_message(body)
178
+ return nil unless body.is_a?(Hash)
179
+
180
+ %w[message statusMessage statusText].filter_map { |key| body[key] }
181
+ .map { |value| value.to_s.strip }
182
+ .find { |value| !value.empty? }
96
183
  end
97
184
 
98
185
  def parse_body(body)
data/lib/sink/error.rb CHANGED
@@ -4,10 +4,41 @@ module Sink
4
4
  class Error < StandardError
5
5
  attr_reader :status, :body
6
6
 
7
- def initialize(status:, message:, body: nil)
7
+ def self.for(status:, message:, body: nil)
8
+ klass = STATUS_ERRORS[status] || (status.to_i >= 500 ? ServerError : self)
9
+ klass.new(message, status: status, body: body)
10
+ end
11
+
12
+ def initialize(message = nil, status: nil, body: nil)
8
13
  @status = status
9
14
  @body = body
10
15
  super(message)
11
16
  end
17
+
18
+ def data = body.is_a?(Hash) ? body["data"] : nil
19
+ end
20
+
21
+ class ValidationError < Error; end
22
+ class Unauthorized < Error; end
23
+ class Forbidden < Error; end
24
+ class NotFound < Error; end
25
+ class Conflict < Error; end
26
+ class StorageNotReady < Error; end
27
+ class RateLimited < Error; end
28
+ class ServerError < Error; end
29
+ class ConnectionError < Error; end
30
+ class TimeoutError < ConnectionError; end
31
+
32
+ class Error
33
+ STATUS_ERRORS = {
34
+ 400 => ValidationError,
35
+ 401 => Unauthorized,
36
+ 403 => Forbidden,
37
+ 404 => NotFound,
38
+ 409 => Conflict,
39
+ 422 => ValidationError,
40
+ 423 => StorageNotReady,
41
+ 429 => RateLimited
42
+ }.freeze
12
43
  end
13
44
  end
data/lib/sink/keys.rb ADDED
@@ -0,0 +1,30 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Sink
4
+ module Keys
5
+ module_function
6
+
7
+ def underscore(key)
8
+ key.to_s
9
+ .gsub(/([A-Z]+)([A-Z][a-z])/, '\1_\2')
10
+ .gsub(/([a-z\d])([A-Z])/, '\1_\2')
11
+ .downcase
12
+ .to_sym
13
+ end
14
+
15
+ def camelize(key)
16
+ head, *tail = key.to_s.split("_")
17
+ [head, *tail.map(&:capitalize)].join
18
+ end
19
+
20
+ # Sink payloads are flat, so nested hashes such as `geo` keep their original
21
+ # keys (country codes) instead of being converted to snake_case.
22
+ def underscore_keys(hash)
23
+ hash.to_h { |key, value| [underscore(key), value] }
24
+ end
25
+
26
+ def camelize_keys(hash)
27
+ hash.to_h { |key, value| [camelize(key), value] }
28
+ end
29
+ end
30
+ end
data/lib/sink/link.rb ADDED
@@ -0,0 +1,79 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Sink
4
+ class Link
5
+ ATTRIBUTES = %i[
6
+ id url slug comment title description image apple google password
7
+ cloaking redirect_with_query unsafe geo tags expiration
8
+ created_at updated_at short_link status
9
+ ].freeze
10
+
11
+ attr_reader :attributes
12
+
13
+ # The API only builds `shortLink` for write endpoints, from the protocol and
14
+ # host of the request, so `base_url` reproduces it for the read endpoints.
15
+ def self.from_response(body, base_url: nil)
16
+ return nil unless body.is_a?(Hash)
17
+
18
+ link = body["link"].is_a?(Hash) ? body["link"] : body
19
+ attributes = Keys.underscore_keys(link)
20
+ attributes[:status] = body["status"] if body.key?("status")
21
+ attributes[:short_link] = body["shortLink"] ||
22
+ (base_url && attributes[:slug] && "#{base_url}/#{attributes[:slug]}")
23
+
24
+ new(attributes.compact)
25
+ end
26
+
27
+ def initialize(attributes)
28
+ @attributes = attributes.freeze
29
+ end
30
+
31
+ %i[id url slug comment title description image apple google password short_link status].each do |name|
32
+ define_method(name) { @attributes[name] }
33
+ end
34
+
35
+ def tags = @attributes[:tags] || []
36
+
37
+ def geo = @attributes[:geo] || {}
38
+
39
+ def expiration = @attributes[:expiration]
40
+
41
+ def created_at = time(:created_at)
42
+
43
+ def updated_at = time(:updated_at)
44
+
45
+ def expires_at = time(:expiration)
46
+
47
+ def cloaking? = !!@attributes[:cloaking]
48
+
49
+ def unsafe? = !!@attributes[:unsafe]
50
+
51
+ def redirect_with_query? = !!@attributes[:redirect_with_query]
52
+
53
+ def expired? = !expiration.nil? && expiration <= Time.now.to_i
54
+
55
+ def created? = status == "created"
56
+
57
+ def existing? = status == "existing"
58
+
59
+ def [](key) = @attributes[key.to_sym]
60
+
61
+ def to_h = @attributes.dup
62
+
63
+ def as_json(*) = to_h
64
+
65
+ def ==(other) = other.is_a?(Link) && other.attributes == @attributes
66
+ alias eql? ==
67
+
68
+ def hash = @attributes.hash
69
+
70
+ def inspect = "#<#{self.class} slug=#{slug.inspect} url=#{url.inspect}>"
71
+
72
+ private
73
+
74
+ def time(key)
75
+ value = @attributes[key]
76
+ Time.at(value).utc if value
77
+ end
78
+ end
79
+ end
data/lib/sink/page.rb ADDED
@@ -0,0 +1,32 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Sink
4
+ class Page
5
+ include Enumerable
6
+
7
+ attr_reader :records, :cursor
8
+
9
+ def initialize(records:, cursor: nil, complete: true)
10
+ @records = records.freeze
11
+ @cursor = cursor
12
+ @complete = complete
13
+ end
14
+
15
+ def each(&) = @records.each(&)
16
+
17
+ def complete? = !!@complete
18
+
19
+ def more? = !complete? && !cursor.nil?
20
+
21
+ def size = @records.size
22
+ alias length size
23
+
24
+ def empty? = @records.empty?
25
+
26
+ def [](index) = @records[index]
27
+
28
+ def to_a = @records.dup
29
+
30
+ def inspect = "#<#{self.class} size=#{size} cursor=#{cursor.inspect} complete=#{complete?}>"
31
+ end
32
+ end
data/lib/sink/tag.rb ADDED
@@ -0,0 +1,13 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Sink
4
+ Tag = Data.define(:name, :count) do
5
+ def self.from_response(body)
6
+ return new(name: body.to_s, count: nil) unless body.is_a?(Hash)
7
+
8
+ new(name: body["name"], count: body["count"])
9
+ end
10
+
11
+ def to_s = name.to_s
12
+ end
13
+ end
data/lib/sink/version.rb CHANGED
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Sink
4
- VERSION = "0.1.2"
4
+ VERSION = "0.2.1"
5
5
  end
data/lib/sink.rb CHANGED
@@ -2,6 +2,10 @@
2
2
 
3
3
  require_relative "sink/version"
4
4
  require_relative "sink/error"
5
+ require_relative "sink/keys"
6
+ require_relative "sink/link"
7
+ require_relative "sink/page"
8
+ require_relative "sink/tag"
5
9
  require_relative "sink/client"
6
10
  require "thread"
7
11
 
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: sink-cool
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.1.2
4
+ version: 0.2.1
5
5
  platform: ruby
6
6
  authors:
7
7
  - Sink Ruby contributors
@@ -37,6 +37,34 @@ dependencies:
37
37
  - - "~>"
38
38
  - !ruby/object:Gem::Version
39
39
  version: '13.0'
40
+ - !ruby/object:Gem::Dependency
41
+ name: simplecov
42
+ requirement: !ruby/object:Gem::Requirement
43
+ requirements:
44
+ - - "~>"
45
+ - !ruby/object:Gem::Version
46
+ version: '0.22'
47
+ type: :development
48
+ prerelease: false
49
+ version_requirements: !ruby/object:Gem::Requirement
50
+ requirements:
51
+ - - "~>"
52
+ - !ruby/object:Gem::Version
53
+ version: '0.22'
54
+ - !ruby/object:Gem::Dependency
55
+ name: simplecov_json_formatter
56
+ requirement: !ruby/object:Gem::Requirement
57
+ requirements:
58
+ - - "~>"
59
+ - !ruby/object:Gem::Version
60
+ version: '0.1'
61
+ type: :development
62
+ prerelease: false
63
+ version_requirements: !ruby/object:Gem::Requirement
64
+ requirements:
65
+ - - "~>"
66
+ - !ruby/object:Gem::Version
67
+ version: '0.1'
40
68
  description: A lightweight Ruby client for managing links with the Sink URL shortener
41
69
  API.
42
70
  executables: []
@@ -50,6 +78,10 @@ files:
50
78
  - lib/sink.rb
51
79
  - lib/sink/client.rb
52
80
  - lib/sink/error.rb
81
+ - lib/sink/keys.rb
82
+ - lib/sink/link.rb
83
+ - lib/sink/page.rb
84
+ - lib/sink/tag.rb
53
85
  - lib/sink/version.rb
54
86
  homepage: https://docs.sink.cool/api/
55
87
  licenses: