sink-cool 0.1.2 → 0.2.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: 1b80f9d22b726100b1e3e5b1ee1dd8db7e4de9bd154a33cfb42c2797333d1b8b
4
- data.tar.gz: b7f8f0dab846a2f53bc5aa5704a6408c7a7c496276c9a60a1335fbb127e2829e
3
+ metadata.gz: 2a7189eda4d5a284c80c198aeb86cfffb50a055a8ad3c9e2eec91b2dd9560f1c
4
+ data.tar.gz: 5cbd0afad6e397979edeeb86f80d29f7bb85d17474163d5ffc60283a527b32ed
5
5
  SHA512:
6
- metadata.gz: 50c9749e0271c7d9dcc870f515e773b352355db3e347b7e7d5a609cd21a36525f2a2ef86c57686ca4883dc69da845a2c6cd110da2bf219ca61d87a20bd91a023
7
- data.tar.gz: e364f3d9217a4bbeeced34575d509b0699ffbbba93150fb0a15f6f989094c4a10ba4a038ff4ec99b0c4483878c3e03d0b3cf9ad218a757c4618bf34956a2c3a8
6
+ metadata.gz: 4ba8fd2a4f777dcbc73d70692e920627500fd6beea73fa9a1034fc4d714e97594118b254fd8d877f8c349755cedb98fcd7dbf15995e40bee0ee6b73fdd7aab92
7
+ data.tar.gz: f195221d617d7135ac222079426989d48ebaa58ea98c83b0b77abc7dadc79b39d6de6717bc9692ed65aedfa1f3686ad6f99ac9f84c60c395656617005f4a7682
data/CHANGELOG.md CHANGED
@@ -4,6 +4,33 @@ All notable changes to this project will be documented in this file.
4
4
 
5
5
  ## Unreleased
6
6
 
7
+ ## 0.2.0 - 2026-07-24
8
+
9
+ ### Added
10
+
11
+ - `Sink::Link` value objects with snake_case readers, `Time` timestamps, predicate methods, `to_h`, and `[]` access to fields without a reader.
12
+ - `Sink::Page` collections including `Enumerable`, exposing `cursor`, `complete?`, and `more?`.
13
+ - `Sink::Tag` records with `name` and `count`.
14
+ - `Client#each_link` to iterate every page without handling cursors manually.
15
+ - Error subclasses for each API failure mode: `Sink::ValidationError`, `Unauthorized`, `Forbidden`, `NotFound`, `Conflict`, `StorageNotReady`, `RateLimited`, `ServerError`, `ConnectionError`, and `TimeoutError`.
16
+ - `Sink::Error#data` for the validation details returned by the API.
17
+
18
+ ### Changed
19
+
20
+ - **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.
21
+ - **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`.
22
+ - **Breaking:** `create_link`, `edit_link`, and `upsert_link` take keyword arguments, with `url` required by all three and `slug` also required by `edit_link`.
23
+ - **Breaking:** `delete_link` returns `true` instead of `nil`.
24
+ - **Breaking:** `verify` returns a hash with snake_case symbol keys.
25
+ - `expires_at` accepts a `Time`, a `Date`, or a unix timestamp and is sent as `expiration`.
26
+ - Timeouts, TLS, and socket failures raise `Sink::Error` subclasses instead of leaking `Net::HTTP` exceptions.
27
+
28
+ ### Fixed
29
+
30
+ - Skip blank `message` values when building error messages, so the Sink API `statusMessage` is no longer replaced by an empty string.
31
+ - Read `statusText` as an error message source, matching newer Sink releases.
32
+ - Validate the token before the base URL so an empty token reports the right argument.
33
+
7
34
  ## 0.1.2 - 2026-07-23
8
35
 
9
36
  ### 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,107 @@ 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
+ `link(slug)` responses carry no short URL, so `short_link` is `nil` there.
112
+
113
+ `upsert_link` never overwrites an existing slug, so check which happened:
114
+
115
+ ```ruby
116
+ link = client.upsert_link(url: "https://example.com", slug: "example")
117
+
118
+ link.created? # => true when the link was stored
119
+ link.existing? # => true when the slug already pointed somewhere else
120
+ ```
121
+
122
+ `delete_link` returns `true`.
123
+
124
+ ### Collections
125
+
126
+ `links`, `search_links`, and `check_links` return a `Sink::Page`, which is
127
+ `Enumerable`:
128
+
129
+ ```ruby
130
+ page = client.links(limit: 20)
131
+
132
+ page.map(&:slug)
133
+ page.cursor # => "..." pagination cursor
134
+ page.complete? # => false when more pages are available
135
+ page.more? # => true when a following page can be fetched
136
+ ```
137
+
138
+ Use `each_link` to walk every page without handling cursors:
139
+
140
+ ```ruby
141
+ client.each_link(tag: "articles") { |link| puts link.short_link }
142
+ client.each_link.lazy.select(&:expired?).first(10)
143
+ ```
144
+
145
+ `tags` returns `Sink::Tag` records with `name` and `count`.
86
146
 
87
147
  ## Errors
88
148
 
89
- Non-successful responses raise `Sink::Error`:
149
+ Non-successful responses raise a subclass of `Sink::Error`, so a Rails
150
+ controller can rescue the cases it cares about:
151
+
152
+ ```ruby
153
+ class LinksController < ApplicationController
154
+ rescue_from Sink::NotFound, with: :not_found
155
+
156
+ def create
157
+ link = Sink.client.create_link(url: params[:url], slug: params[:slug])
158
+ render json: link.to_h, status: :created
159
+ rescue Sink::Conflict
160
+ render json: { error: "slug already taken" }, status: :conflict
161
+ end
162
+ end
163
+ ```
164
+
165
+ | Class | Raised for |
166
+ | --- | --- |
167
+ | `Sink::ValidationError` | 400, 422 (`error.data` holds the API details) |
168
+ | `Sink::Unauthorized` | 401, usually a wrong site token |
169
+ | `Sink::Forbidden` | 403, including preview mode |
170
+ | `Sink::NotFound` | 404 |
171
+ | `Sink::Conflict` | 409, the slug already exists |
172
+ | `Sink::StorageNotReady` | 423, open Dashboard → Links once after deploying |
173
+ | `Sink::RateLimited` | 429 |
174
+ | `Sink::ServerError` | 5xx |
175
+ | `Sink::TimeoutError` | open or read timeout |
176
+ | `Sink::ConnectionError` | DNS, TLS, and socket failures |
177
+
178
+ Every error exposes `status`, `message`, `body`, and `data`:
90
179
 
91
180
  ```ruby
92
181
  begin
93
182
  Sink.client.link("missing")
94
183
  rescue Sink::Error => error
95
- error.status
96
- error.message
97
- error.body
184
+ error.status # => 404
185
+ error.message # => "Not Found"
186
+ error.body # => parsed response body
98
187
  end
99
188
  ```
100
189
 
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,119 @@ 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
+ Link.from_response(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
+ Link.from_response(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
+ Link.from_response(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) = Link.from_response(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 link_page(body) = page(body, "links") { |link| Link.from_response(link) }
118
+
119
+ def page(body, key, &build)
120
+ records, cursor, complete = case body
121
+ when Array then [body, nil, true]
122
+ when Hash then [Array(body[key]), body["cursor"], body.fetch("list_complete", true)]
123
+ else [[], nil, true]
124
+ end
125
+
126
+ Page.new(records: records.map(&build), cursor: cursor, complete: complete)
127
+ end
128
+
57
129
  def request(method, path, query: nil, body: nil)
58
130
  uri = URI("#{@base_url}#{path}")
59
131
  params = query&.compact
@@ -69,7 +141,15 @@ module Sink
69
141
  http_request = REQUEST_CLASSES.fetch(method).new(uri, headers)
70
142
  http_request.body = JSON.generate(body) if body
71
143
 
72
- parse_response(perform_request(uri, http_request))
144
+ parse_response(dispatch(uri, http_request))
145
+ end
146
+
147
+ def dispatch(uri, http_request)
148
+ perform_request(uri, http_request)
149
+ rescue *TIMEOUT_ERRORS => error
150
+ raise TimeoutError.new("#{uri.host} timed out: #{error.message}")
151
+ rescue *CONNECTION_ERRORS => error
152
+ raise ConnectionError.new("#{uri.host} is unreachable: #{error.message}")
73
153
  end
74
154
 
75
155
  def perform_request(uri, request)
@@ -88,11 +168,16 @@ module Sink
88
168
  body = parse_body(response.body)
89
169
  return body if response.is_a?(Net::HTTPSuccess)
90
170
 
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)
171
+ message = error_message(body) || "HTTP #{response.code} #{response.message}"
172
+ raise Error.for(status: response.code.to_i, message: message, body: body)
173
+ end
174
+
175
+ def error_message(body)
176
+ return nil unless body.is_a?(Hash)
177
+
178
+ %w[message statusMessage statusText].filter_map { |key| body[key] }
179
+ .map { |value| value.to_s.strip }
180
+ .find { |value| !value.empty? }
96
181
  end
97
182
 
98
183
  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,76 @@
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
+ def self.from_response(body)
14
+ return nil unless body.is_a?(Hash)
15
+
16
+ link = body["link"].is_a?(Hash) ? body["link"] : body
17
+ attributes = Keys.underscore_keys(link)
18
+ attributes[:short_link] = body["shortLink"] if body.key?("shortLink")
19
+ attributes[:status] = body["status"] if body.key?("status")
20
+
21
+ new(attributes)
22
+ end
23
+
24
+ def initialize(attributes)
25
+ @attributes = attributes.freeze
26
+ end
27
+
28
+ %i[id url slug comment title description image apple google password short_link status].each do |name|
29
+ define_method(name) { @attributes[name] }
30
+ end
31
+
32
+ def tags = @attributes[:tags] || []
33
+
34
+ def geo = @attributes[:geo] || {}
35
+
36
+ def expiration = @attributes[:expiration]
37
+
38
+ def created_at = time(:created_at)
39
+
40
+ def updated_at = time(:updated_at)
41
+
42
+ def expires_at = time(:expiration)
43
+
44
+ def cloaking? = !!@attributes[:cloaking]
45
+
46
+ def unsafe? = !!@attributes[:unsafe]
47
+
48
+ def redirect_with_query? = !!@attributes[:redirect_with_query]
49
+
50
+ def expired? = !expiration.nil? && expiration <= Time.now.to_i
51
+
52
+ def created? = status == "created"
53
+
54
+ def existing? = status == "existing"
55
+
56
+ def [](key) = @attributes[key.to_sym]
57
+
58
+ def to_h = @attributes.dup
59
+
60
+ def as_json(*) = to_h
61
+
62
+ def ==(other) = other.is_a?(Link) && other.attributes == @attributes
63
+ alias eql? ==
64
+
65
+ def hash = @attributes.hash
66
+
67
+ def inspect = "#<#{self.class} slug=#{slug.inspect} url=#{url.inspect}>"
68
+
69
+ private
70
+
71
+ def time(key)
72
+ value = @attributes[key]
73
+ Time.at(value).utc if value
74
+ end
75
+ end
76
+ 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.0"
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.0
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: