wintertc 0.1.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 +7 -0
- data/CHANGELOG.md +19 -0
- data/LICENSE +21 -0
- data/README.md +137 -0
- data/lib/wintertc/error.rb +15 -0
- data/lib/wintertc/fetch.rb +182 -0
- data/lib/wintertc/headers.rb +139 -0
- data/lib/wintertc/request.rb +86 -0
- data/lib/wintertc/response.rb +146 -0
- data/lib/wintertc/version.rb +6 -0
- data/lib/wintertc.rb +44 -0
- data/sig/wintertc.rbs +110 -0
- metadata +85 -0
checksums.yaml
ADDED
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
---
|
|
2
|
+
SHA256:
|
|
3
|
+
metadata.gz: 6d89343a04cf6097d2229ad2d14423636f3909bc5e101e07d2d3d0787ac0f272
|
|
4
|
+
data.tar.gz: ff8a77f280384bcf57b63763173dffa1fa7f29fd2edd6d3654f18cf9490ed50a
|
|
5
|
+
SHA512:
|
|
6
|
+
metadata.gz: a1ac56ebd31fa3411fb4cfa34ed461d0207a5c5a67f0fdc0f8f9e85d3a8ee96a19d349738921a5ffee12a82f46a5370bc4b89b2c01bd505e8c52e59d1530734d
|
|
7
|
+
data.tar.gz: ac43fdc877c54158e05ded060284a2d5700e66539bf9af887bd8d4e58787f977cd1dab9f5fe4cd4bfd96f04f8332021fb9b8a5aae6eb297b32573a6cb3fad9c4
|
data/CHANGELOG.md
ADDED
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
# Changelog
|
|
2
|
+
|
|
3
|
+
All notable changes to this project will be documented in this file.
|
|
4
|
+
|
|
5
|
+
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
|
|
6
|
+
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
|
7
|
+
|
|
8
|
+
## [Unreleased]
|
|
9
|
+
|
|
10
|
+
## [0.1.0] - 2026-03-08
|
|
11
|
+
|
|
12
|
+
### Added
|
|
13
|
+
- Initial release.
|
|
14
|
+
- `WinterTc::Headers` — case-insensitive HTTP header map.
|
|
15
|
+
- `WinterTc::Request` — immutable HTTP request object.
|
|
16
|
+
- `WinterTc::Response` — HTTP response with `#text`, `#json`, `#ok` helpers, and `.json` static constructor.
|
|
17
|
+
- `WinterTc.fetch` — synchronous Fetch-API-compatible HTTP client.
|
|
18
|
+
- RBS type signatures in `sig/wintertc.rbs`.
|
|
19
|
+
- GitHub Actions CI for Ruby 3.3, 3.4, and head.
|
data/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 kuboon
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
data/README.md
ADDED
|
@@ -0,0 +1,137 @@
|
|
|
1
|
+
# wintertc.rb
|
|
2
|
+
|
|
3
|
+
[](https://github.com/kuboon/wintertc.rb/actions/workflows/ci.yml)
|
|
4
|
+
[](https://badge.fury.io/rb/wintertc)
|
|
5
|
+
|
|
6
|
+
A Ruby HTTP client whose interface mirrors the JavaScript
|
|
7
|
+
[Fetch API](https://developer.mozilla.org/en-US/docs/Web/API/Fetch_API) as
|
|
8
|
+
closely as possible.
|
|
9
|
+
|
|
10
|
+
**No runtime gem dependencies** — only Ruby's built-in `net/http`, `uri`, and
|
|
11
|
+
`json` standard-library modules are used.
|
|
12
|
+
|
|
13
|
+
## Installation
|
|
14
|
+
|
|
15
|
+
Add to your `Gemfile`:
|
|
16
|
+
|
|
17
|
+
```ruby
|
|
18
|
+
gem "wintertc"
|
|
19
|
+
```
|
|
20
|
+
|
|
21
|
+
or install directly:
|
|
22
|
+
|
|
23
|
+
```
|
|
24
|
+
gem install wintertc
|
|
25
|
+
```
|
|
26
|
+
|
|
27
|
+
## Quick Start
|
|
28
|
+
|
|
29
|
+
```ruby
|
|
30
|
+
require "wintertc"
|
|
31
|
+
|
|
32
|
+
# Simple GET
|
|
33
|
+
response = WinterTc.fetch("https://httpbin.org/get")
|
|
34
|
+
puts response.status # => 200
|
|
35
|
+
puts response.ok # => true
|
|
36
|
+
puts response.json["url"] # => "https://httpbin.org/get"
|
|
37
|
+
|
|
38
|
+
# POST with a JSON body
|
|
39
|
+
response = WinterTc.fetch(
|
|
40
|
+
"https://httpbin.org/post",
|
|
41
|
+
method: "POST",
|
|
42
|
+
headers: { "Content-Type" => "application/json" },
|
|
43
|
+
body: JSON.generate({ hello: "world" })
|
|
44
|
+
)
|
|
45
|
+
puts response.json["json"] # => {"hello"=>"world"}
|
|
46
|
+
|
|
47
|
+
# Re-use a Request object
|
|
48
|
+
req = WinterTc::Request.new("https://httpbin.org/get")
|
|
49
|
+
response = WinterTc.fetch(req)
|
|
50
|
+
```
|
|
51
|
+
|
|
52
|
+
## API Reference
|
|
53
|
+
|
|
54
|
+
### `WinterTc.fetch(input, **options) → Response`
|
|
55
|
+
|
|
56
|
+
Performs a synchronous HTTP request and returns a `Response`.
|
|
57
|
+
|
|
58
|
+
| Option | Type | Default | Description |
|
|
59
|
+
|------------|-----------------------------------|------------|-----------------------------------------------------|
|
|
60
|
+
| `method` | `String` | `"GET"` | HTTP verb (GET, POST, PUT, PATCH, DELETE, …) |
|
|
61
|
+
| `headers` | `Hash` / `WinterTc::Headers` | `{}` | Request headers |
|
|
62
|
+
| `body` | `String` / `nil` | `nil` | Request body |
|
|
63
|
+
| `redirect` | `:follow` / `:manual` / `:error` | `:follow` | Redirect strategy |
|
|
64
|
+
|
|
65
|
+
Redirects on 301/302/303 change the method to `GET`; 307/308 preserve the
|
|
66
|
+
original method — consistent with browser behaviour.
|
|
67
|
+
|
|
68
|
+
### `WinterTc::Headers`
|
|
69
|
+
|
|
70
|
+
A case-insensitive header map.
|
|
71
|
+
|
|
72
|
+
```ruby
|
|
73
|
+
h = WinterTc::Headers.new("Content-Type" => "text/html")
|
|
74
|
+
h.get("content-type") # => "text/html"
|
|
75
|
+
h.has("Content-Type") # => true
|
|
76
|
+
h.set("Accept", "application/json")
|
|
77
|
+
h.append("Accept", "text/plain")
|
|
78
|
+
h.get("accept") # => "application/json, text/plain"
|
|
79
|
+
h.delete("accept")
|
|
80
|
+
h.to_h # => {}
|
|
81
|
+
```
|
|
82
|
+
|
|
83
|
+
### `WinterTc::Request`
|
|
84
|
+
|
|
85
|
+
```ruby
|
|
86
|
+
req = WinterTc::Request.new(
|
|
87
|
+
"https://api.example.com/items",
|
|
88
|
+
method: "POST",
|
|
89
|
+
headers: { "Authorization" => "Bearer token" },
|
|
90
|
+
body: JSON.generate({ name: "widget" })
|
|
91
|
+
)
|
|
92
|
+
req.url # => "https://api.example.com/items"
|
|
93
|
+
req.method # => "POST"
|
|
94
|
+
req.headers # => #<WinterTc::Headers ...>
|
|
95
|
+
req.body # => '{"name":"widget"}'
|
|
96
|
+
|
|
97
|
+
# Clone with overrides
|
|
98
|
+
updated = WinterTc::Request.new(req, body: JSON.generate({ name: "gadget" }))
|
|
99
|
+
```
|
|
100
|
+
|
|
101
|
+
### `WinterTc::Response`
|
|
102
|
+
|
|
103
|
+
```ruby
|
|
104
|
+
response.status # => 200
|
|
105
|
+
response.ok # => true (status 200–299)
|
|
106
|
+
response.ok? # alias for ok
|
|
107
|
+
response.status_text # => "OK"
|
|
108
|
+
response.headers # => #<WinterTc::Headers ...>
|
|
109
|
+
response.text # => raw body string
|
|
110
|
+
response.json # => parsed JSON (raises JSON::ParserError on invalid JSON)
|
|
111
|
+
response.url # => final URL after redirects
|
|
112
|
+
```
|
|
113
|
+
|
|
114
|
+
## Error Classes
|
|
115
|
+
|
|
116
|
+
| Class | Raised when |
|
|
117
|
+
|--------------------------------|---------------------------------------------------|
|
|
118
|
+
| `WinterTc::Error` | Base class for all WinterTc errors |
|
|
119
|
+
| `WinterTc::TooManyRedirectsError` | More than `WinterTc::MAX_REDIRECTS` redirects |
|
|
120
|
+
| `WinterTc::RedirectError` | `redirect: :error` and a redirect is received |
|
|
121
|
+
| `WinterTc::UnsupportedMethodError` | An unknown HTTP verb is requested |
|
|
122
|
+
|
|
123
|
+
## Type Signatures (RBS)
|
|
124
|
+
|
|
125
|
+
RBS signatures are shipped in `sig/wintertc.rbs` and are compatible with
|
|
126
|
+
[Steep](https://github.com/soutaro/steep) and other RBS-aware tools.
|
|
127
|
+
|
|
128
|
+
## Development
|
|
129
|
+
|
|
130
|
+
```
|
|
131
|
+
bundle install
|
|
132
|
+
bundle exec rake test
|
|
133
|
+
```
|
|
134
|
+
|
|
135
|
+
## License
|
|
136
|
+
|
|
137
|
+
[MIT](LICENSE)
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module WinterTc
|
|
4
|
+
# Base class for all WinterTc errors.
|
|
5
|
+
class Error < StandardError; end
|
|
6
|
+
|
|
7
|
+
# Raised when more redirects than {WinterTc::MAX_REDIRECTS} are encountered.
|
|
8
|
+
class TooManyRedirectsError < Error; end
|
|
9
|
+
|
|
10
|
+
# Raised when a redirect is encountered and `redirect: :error` is set.
|
|
11
|
+
class RedirectError < Error; end
|
|
12
|
+
|
|
13
|
+
# Raised when an unsupported HTTP method is requested.
|
|
14
|
+
class UnsupportedMethodError < Error; end
|
|
15
|
+
end
|
|
@@ -0,0 +1,182 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "net/http"
|
|
4
|
+
require "uri"
|
|
5
|
+
|
|
6
|
+
module WinterTc
|
|
7
|
+
# The default maximum number of redirects that {WinterTc.fetch} will follow
|
|
8
|
+
# before raising {TooManyRedirectsError}.
|
|
9
|
+
MAX_REDIRECTS = 20
|
|
10
|
+
|
|
11
|
+
# @api private
|
|
12
|
+
# Maps HTTP method strings to Net::HTTP request classes.
|
|
13
|
+
NET_HTTP_METHOD_MAP = {
|
|
14
|
+
"GET" => Net::HTTP::Get,
|
|
15
|
+
"POST" => Net::HTTP::Post,
|
|
16
|
+
"PUT" => Net::HTTP::Put,
|
|
17
|
+
"PATCH" => Net::HTTP::Patch,
|
|
18
|
+
"DELETE" => Net::HTTP::Delete,
|
|
19
|
+
"HEAD" => Net::HTTP::Head,
|
|
20
|
+
"OPTIONS" => Net::HTTP::Options,
|
|
21
|
+
}.freeze
|
|
22
|
+
private_constant :NET_HTTP_METHOD_MAP
|
|
23
|
+
|
|
24
|
+
class << self
|
|
25
|
+
# Performs an HTTP request, mirroring the JavaScript
|
|
26
|
+
# {https://developer.mozilla.org/en-US/docs/Web/API/fetch fetch()} function.
|
|
27
|
+
#
|
|
28
|
+
# Unlike the JavaScript version this method is *synchronous* — it blocks
|
|
29
|
+
# until the server returns a complete response (no Promise / async / await).
|
|
30
|
+
#
|
|
31
|
+
# Redirects are followed automatically by default (up to {MAX_REDIRECTS}
|
|
32
|
+
# hops). On 301 / 302 / 303 responses the method is changed to GET, while
|
|
33
|
+
# 307 / 308 preserve the original method — consistent with browser behaviour.
|
|
34
|
+
#
|
|
35
|
+
# @param input [String, Request] target URL string or a {Request} object.
|
|
36
|
+
# @param method [String, nil] HTTP method (default: +"GET"+). Ignored
|
|
37
|
+
# when +input+ is a Request and +method+ is not explicitly provided.
|
|
38
|
+
# @param headers [Hash, Headers, nil] request headers.
|
|
39
|
+
# @param body [String, nil] request body.
|
|
40
|
+
# @param redirect [Symbol] redirect handling strategy:
|
|
41
|
+
# * +:follow+ (default) — follow redirects transparently.
|
|
42
|
+
# * +:manual+ — return the 3xx response as-is without following.
|
|
43
|
+
# * +:error+ — raise {RedirectError} when a redirect is encountered.
|
|
44
|
+
# @return [Response]
|
|
45
|
+
# @raise [ArgumentError] if the URL scheme is not http or https.
|
|
46
|
+
# @raise [TooManyRedirectsError] if more than {MAX_REDIRECTS} redirects
|
|
47
|
+
# are encountered.
|
|
48
|
+
# @raise [RedirectError] if +redirect: :error+ and a redirect
|
|
49
|
+
# response is returned.
|
|
50
|
+
#
|
|
51
|
+
# @example Simple GET
|
|
52
|
+
# response = WinterTc.fetch("https://example.com")
|
|
53
|
+
# puts response.status #=> 200
|
|
54
|
+
# puts response.ok #=> true
|
|
55
|
+
# puts response.text #=> "<html>..."
|
|
56
|
+
#
|
|
57
|
+
# @example POST with a JSON body
|
|
58
|
+
# response = WinterTc.fetch(
|
|
59
|
+
# "https://api.example.com/items",
|
|
60
|
+
# method: "POST",
|
|
61
|
+
# headers: { "Content-Type" => "application/json" },
|
|
62
|
+
# body: JSON.generate({ name: "widget" })
|
|
63
|
+
# )
|
|
64
|
+
# item = response.json #=> { "id" => 42, "name" => "widget" }
|
|
65
|
+
#
|
|
66
|
+
# @example Re-using a Request object
|
|
67
|
+
# req = WinterTc::Request.new("https://api.example.com/items", method: "GET")
|
|
68
|
+
# response = WinterTc.fetch(req)
|
|
69
|
+
def fetch(input, method: nil, headers: nil, body: nil, redirect: :follow, **_opts)
|
|
70
|
+
request = build_request(input, method: method, headers: headers, body: body)
|
|
71
|
+
perform(request, redirect: redirect, hops: 0)
|
|
72
|
+
end
|
|
73
|
+
|
|
74
|
+
private
|
|
75
|
+
|
|
76
|
+
# Builds a {Request} from a URL string or existing Request, applying
|
|
77
|
+
# any extra keyword-argument overrides.
|
|
78
|
+
def build_request(input, method:, headers:, body:)
|
|
79
|
+
if input.is_a?(Request)
|
|
80
|
+
Request.new(input, method: method, headers: headers, body: body)
|
|
81
|
+
else
|
|
82
|
+
Request.new(input.to_s, method: method, headers: headers, body: body)
|
|
83
|
+
end
|
|
84
|
+
end
|
|
85
|
+
|
|
86
|
+
# Executes the HTTP request, handling redirects recursively.
|
|
87
|
+
def perform(request, redirect:, hops:)
|
|
88
|
+
uri = parse_uri(request.url)
|
|
89
|
+
net_req = build_net_request(request, uri) # validate method before opening a socket
|
|
90
|
+
|
|
91
|
+
net_response = Net::HTTP.start(uri.host, uri.port, use_ssl: uri.scheme == "https") do |http|
|
|
92
|
+
http.request(net_req)
|
|
93
|
+
end
|
|
94
|
+
|
|
95
|
+
if net_response.is_a?(Net::HTTPRedirection)
|
|
96
|
+
handle_redirect(net_response, request, redirect: redirect, hops: hops)
|
|
97
|
+
else
|
|
98
|
+
build_response(net_response, url: request.url)
|
|
99
|
+
end
|
|
100
|
+
end
|
|
101
|
+
|
|
102
|
+
# Parses a URL string into a URI, raising ArgumentError for non-HTTP(S) URLs.
|
|
103
|
+
def parse_uri(url)
|
|
104
|
+
uri = URI.parse(url)
|
|
105
|
+
unless uri.is_a?(URI::HTTP)
|
|
106
|
+
raise ArgumentError, "Only http and https URLs are supported; got: #{url.inspect}"
|
|
107
|
+
end
|
|
108
|
+
|
|
109
|
+
uri
|
|
110
|
+
end
|
|
111
|
+
|
|
112
|
+
# Builds a Net::HTTP request object from a {Request}.
|
|
113
|
+
def build_net_request(request, uri)
|
|
114
|
+
klass = NET_HTTP_METHOD_MAP[request.method] ||
|
|
115
|
+
raise(UnsupportedMethodError, "Unsupported HTTP method: #{request.method}")
|
|
116
|
+
|
|
117
|
+
net_req = klass.new(uri.request_uri)
|
|
118
|
+
request.headers.each { |name, value| net_req[name] = value }
|
|
119
|
+
net_req.body = request.body if request.body
|
|
120
|
+
net_req
|
|
121
|
+
end
|
|
122
|
+
|
|
123
|
+
# Handles a 3xx redirect response according to the +redirect+ strategy.
|
|
124
|
+
def handle_redirect(net_response, original_request, redirect:, hops:)
|
|
125
|
+
case redirect
|
|
126
|
+
when :follow
|
|
127
|
+
if hops >= MAX_REDIRECTS
|
|
128
|
+
raise TooManyRedirectsError,
|
|
129
|
+
"Too many redirects (maximum is #{MAX_REDIRECTS})"
|
|
130
|
+
end
|
|
131
|
+
|
|
132
|
+
location = net_response["location"]
|
|
133
|
+
unless location
|
|
134
|
+
raise RedirectError,
|
|
135
|
+
"Redirect response (#{net_response.code}) is missing a Location header"
|
|
136
|
+
end
|
|
137
|
+
new_url = resolve_url(location, original_request.url)
|
|
138
|
+
|
|
139
|
+
# 301/302/303 → change to GET; 307/308 → preserve original method.
|
|
140
|
+
new_method = case net_response.code.to_i
|
|
141
|
+
when 307, 308 then original_request.method
|
|
142
|
+
else "GET"
|
|
143
|
+
end
|
|
144
|
+
request_kwargs = { method: new_method, headers: original_request.headers }
|
|
145
|
+
request_kwargs[:body] = original_request.body if new_method == original_request.method && original_request.body
|
|
146
|
+
new_request = Request.new(new_url, **request_kwargs)
|
|
147
|
+
perform(new_request, redirect: :follow, hops: hops + 1)
|
|
148
|
+
|
|
149
|
+
when :error
|
|
150
|
+
raise RedirectError,
|
|
151
|
+
"Unexpected redirect (#{net_response.code}) to #{net_response["location"]}"
|
|
152
|
+
|
|
153
|
+
when :manual
|
|
154
|
+
build_response(net_response, url: original_request.url)
|
|
155
|
+
|
|
156
|
+
else
|
|
157
|
+
raise ArgumentError, "Unknown redirect option: #{redirect.inspect}"
|
|
158
|
+
end
|
|
159
|
+
end
|
|
160
|
+
|
|
161
|
+
# Converts a Net::HTTPResponse into a {Response}.
|
|
162
|
+
def build_response(net_response, url:)
|
|
163
|
+
headers = Headers.new
|
|
164
|
+
net_response.each_header { |k, v| headers.set(k, v) }
|
|
165
|
+
Response.new(
|
|
166
|
+
status: net_response.code.to_i,
|
|
167
|
+
headers: headers,
|
|
168
|
+
body: net_response.body,
|
|
169
|
+
url: url,
|
|
170
|
+
)
|
|
171
|
+
end
|
|
172
|
+
|
|
173
|
+
# Resolves a (possibly relative) redirect location against the base URL.
|
|
174
|
+
def resolve_url(location, base_url)
|
|
175
|
+
if location.start_with?("http://", "https://")
|
|
176
|
+
location
|
|
177
|
+
else
|
|
178
|
+
URI.join(base_url, location).to_s
|
|
179
|
+
end
|
|
180
|
+
end
|
|
181
|
+
end
|
|
182
|
+
end
|
|
@@ -0,0 +1,139 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module WinterTc
|
|
4
|
+
# A case-insensitive map of HTTP headers, mirroring the JavaScript
|
|
5
|
+
# {https://developer.mozilla.org/en-US/docs/Web/API/Headers Headers} interface.
|
|
6
|
+
#
|
|
7
|
+
# Header names are normalised to lowercase internally so that lookups are
|
|
8
|
+
# always case-insensitive, just like the browser implementation.
|
|
9
|
+
#
|
|
10
|
+
# @example Basic usage
|
|
11
|
+
# headers = WinterTc::Headers.new("Content-Type" => "application/json")
|
|
12
|
+
# headers.get("content-type") #=> "application/json"
|
|
13
|
+
# headers.has("CONTENT-TYPE") #=> true
|
|
14
|
+
# headers.set("Accept", "text/html")
|
|
15
|
+
# headers.append("Accept", "application/xhtml+xml")
|
|
16
|
+
# headers.get("accept") #=> "text/html, application/xhtml+xml"
|
|
17
|
+
class Headers
|
|
18
|
+
include Enumerable
|
|
19
|
+
|
|
20
|
+
# Creates a new Headers object.
|
|
21
|
+
#
|
|
22
|
+
# @param init [Hash, Array<Array<String>>, Headers, nil]
|
|
23
|
+
# Initial headers. Accepts a Hash (String => String), an Array of
|
|
24
|
+
# two-element [name, value] arrays, another Headers object, or nil for
|
|
25
|
+
# an empty collection.
|
|
26
|
+
# @raise [TypeError] when init is not one of the accepted types
|
|
27
|
+
def initialize(init = nil)
|
|
28
|
+
@data = {}
|
|
29
|
+
case init
|
|
30
|
+
when Hash then init.each { |k, v| set(k, v) }
|
|
31
|
+
when Array then init.each { |(k, v)| set(k, v) }
|
|
32
|
+
when Headers then init.each { |k, v| @data[k] = v }
|
|
33
|
+
when nil then # empty
|
|
34
|
+
else raise TypeError, "init must be a Hash, Array, Headers, or nil"
|
|
35
|
+
end
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
# Returns the first value associated with the given header name, or +nil+
|
|
39
|
+
# if the header is not present.
|
|
40
|
+
#
|
|
41
|
+
# @param name [String] header name (case-insensitive)
|
|
42
|
+
# @return [String, nil]
|
|
43
|
+
def get(name)
|
|
44
|
+
@data[normalize(name)]
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
# Sets a header, replacing any existing value.
|
|
48
|
+
#
|
|
49
|
+
# @param name [String] header name (case-insensitive)
|
|
50
|
+
# @param value [String] header value
|
|
51
|
+
# @return [void]
|
|
52
|
+
def set(name, value)
|
|
53
|
+
@data[normalize(name)] = value.to_s
|
|
54
|
+
nil
|
|
55
|
+
end
|
|
56
|
+
|
|
57
|
+
# Returns +true+ if a header with the given name exists.
|
|
58
|
+
#
|
|
59
|
+
# @param name [String] header name (case-insensitive)
|
|
60
|
+
# @return [Boolean]
|
|
61
|
+
def has(name)
|
|
62
|
+
@data.key?(normalize(name))
|
|
63
|
+
end
|
|
64
|
+
|
|
65
|
+
# Removes the header with the given name.
|
|
66
|
+
#
|
|
67
|
+
# @param name [String] header name (case-insensitive)
|
|
68
|
+
# @return [void]
|
|
69
|
+
def delete(name)
|
|
70
|
+
@data.delete(normalize(name))
|
|
71
|
+
nil
|
|
72
|
+
end
|
|
73
|
+
|
|
74
|
+
# Appends a value to an existing header. If the header is not yet
|
|
75
|
+
# present it is created. Multiple values are joined with +", "+,
|
|
76
|
+
# following the HTTP specification.
|
|
77
|
+
#
|
|
78
|
+
# @param name [String] header name (case-insensitive)
|
|
79
|
+
# @param value [String] value to append
|
|
80
|
+
# @return [void]
|
|
81
|
+
def append(name, value)
|
|
82
|
+
key = normalize(name)
|
|
83
|
+
if @data.key?(key)
|
|
84
|
+
@data[key] = "#{@data[key]}, #{value}"
|
|
85
|
+
else
|
|
86
|
+
@data[key] = value.to_s
|
|
87
|
+
end
|
|
88
|
+
nil
|
|
89
|
+
end
|
|
90
|
+
|
|
91
|
+
# Yields each +[name, value]+ pair. Names are in lowercase.
|
|
92
|
+
#
|
|
93
|
+
# @yieldparam name [String]
|
|
94
|
+
# @yieldparam value [String]
|
|
95
|
+
# @return [Enumerator] if no block is given
|
|
96
|
+
# @return [self] otherwise
|
|
97
|
+
def each
|
|
98
|
+
return to_enum(:each) unless block_given?
|
|
99
|
+
|
|
100
|
+
@data.each do |name, value|
|
|
101
|
+
yield name, value
|
|
102
|
+
end
|
|
103
|
+
self
|
|
104
|
+
end
|
|
105
|
+
|
|
106
|
+
# Returns all header names (lowercase).
|
|
107
|
+
#
|
|
108
|
+
# @return [Array<String>]
|
|
109
|
+
def keys
|
|
110
|
+
@data.keys
|
|
111
|
+
end
|
|
112
|
+
|
|
113
|
+
# Returns all header values.
|
|
114
|
+
#
|
|
115
|
+
# @return [Array<String>]
|
|
116
|
+
def values
|
|
117
|
+
@data.values
|
|
118
|
+
end
|
|
119
|
+
|
|
120
|
+
# Returns a plain +Hash+ copy of all headers.
|
|
121
|
+
#
|
|
122
|
+
# @return [Hash{String => String}]
|
|
123
|
+
def to_h
|
|
124
|
+
@data.dup
|
|
125
|
+
end
|
|
126
|
+
|
|
127
|
+
# @return [String]
|
|
128
|
+
def inspect
|
|
129
|
+
"#<#{self.class} #{@data.inspect}>"
|
|
130
|
+
end
|
|
131
|
+
|
|
132
|
+
private
|
|
133
|
+
|
|
134
|
+
# Normalises a header name to lowercase.
|
|
135
|
+
def normalize(name)
|
|
136
|
+
name.to_s.downcase
|
|
137
|
+
end
|
|
138
|
+
end
|
|
139
|
+
end
|
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module WinterTc
|
|
4
|
+
# Represents an HTTP request, mirroring the JavaScript
|
|
5
|
+
# {https://developer.mozilla.org/en-US/docs/Web/API/Request Request} interface.
|
|
6
|
+
#
|
|
7
|
+
# A Request object can be passed directly to {WinterTc.fetch} instead of a
|
|
8
|
+
# plain URL string. It can also be used to clone an existing request while
|
|
9
|
+
# overriding individual fields.
|
|
10
|
+
#
|
|
11
|
+
# @example Creating a POST request
|
|
12
|
+
# req = WinterTc::Request.new(
|
|
13
|
+
# "https://example.com/api",
|
|
14
|
+
# method: "POST",
|
|
15
|
+
# headers: { "Content-Type" => "application/json" },
|
|
16
|
+
# body: JSON.generate({ key: "value" })
|
|
17
|
+
# )
|
|
18
|
+
# response = WinterTc.fetch(req)
|
|
19
|
+
#
|
|
20
|
+
# @example Cloning a request with a different body
|
|
21
|
+
# cloned = WinterTc::Request.new(req, body: JSON.generate({ key: "new" }))
|
|
22
|
+
class Request
|
|
23
|
+
# The absolute URL of the request.
|
|
24
|
+
# @return [String]
|
|
25
|
+
attr_reader :url
|
|
26
|
+
|
|
27
|
+
# The HTTP method in uppercase, e.g. +"GET"+ or +"POST"+.
|
|
28
|
+
# @return [String]
|
|
29
|
+
attr_reader :method
|
|
30
|
+
|
|
31
|
+
# The request headers.
|
|
32
|
+
# @return [Headers]
|
|
33
|
+
attr_reader :headers
|
|
34
|
+
|
|
35
|
+
# The request body, or +nil+ for requests without a body.
|
|
36
|
+
# @return [String, nil]
|
|
37
|
+
attr_reader :body
|
|
38
|
+
|
|
39
|
+
# Creates a new Request.
|
|
40
|
+
#
|
|
41
|
+
# @param input [String, Request] the target URL or an existing Request to
|
|
42
|
+
# clone.
|
|
43
|
+
# @param method [String, nil] HTTP method. Defaults to +"GET"+, or to
|
|
44
|
+
# the method of the cloned request.
|
|
45
|
+
# @param headers [Hash, Headers, nil] Additional headers. When cloning a
|
|
46
|
+
# request the headers are merged: these values take precedence.
|
|
47
|
+
# @param body [String, nil] Request body. Defaults to +nil+, or to the
|
|
48
|
+
# body of the cloned request.
|
|
49
|
+
# @raise [TypeError] when input is not a String or Request
|
|
50
|
+
def initialize(input, method: nil, headers: nil, body: nil)
|
|
51
|
+
case input
|
|
52
|
+
when Request
|
|
53
|
+
@url = input.url
|
|
54
|
+
@method = (method || input.method).to_s.upcase
|
|
55
|
+
@headers = merge_headers(input.headers, headers)
|
|
56
|
+
@body = body.nil? ? input.body : body
|
|
57
|
+
when String
|
|
58
|
+
@url = input
|
|
59
|
+
@method = (method || "GET").to_s.upcase
|
|
60
|
+
@headers = Headers.new(headers)
|
|
61
|
+
@body = body
|
|
62
|
+
else
|
|
63
|
+
raise TypeError, "input must be a String URL or a Request; got #{input.class}"
|
|
64
|
+
end
|
|
65
|
+
end
|
|
66
|
+
|
|
67
|
+
# @return [String]
|
|
68
|
+
def inspect
|
|
69
|
+
"#<#{self.class} #{@method} #{@url}>"
|
|
70
|
+
end
|
|
71
|
+
|
|
72
|
+
private
|
|
73
|
+
|
|
74
|
+
# Merges base headers with optional overrides, returning a new Headers.
|
|
75
|
+
def merge_headers(base, extra)
|
|
76
|
+
h = Headers.new(base)
|
|
77
|
+
return h unless extra
|
|
78
|
+
|
|
79
|
+
case extra
|
|
80
|
+
when Headers then extra.each { |k, v| h.set(k, v) }
|
|
81
|
+
when Hash then extra.each { |k, v| h.set(k, v) }
|
|
82
|
+
end
|
|
83
|
+
h
|
|
84
|
+
end
|
|
85
|
+
end
|
|
86
|
+
end
|
|
@@ -0,0 +1,146 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "json"
|
|
4
|
+
|
|
5
|
+
module WinterTc
|
|
6
|
+
# Represents an HTTP response, mirroring the JavaScript
|
|
7
|
+
# {https://developer.mozilla.org/en-US/docs/Web/API/Response Response} interface.
|
|
8
|
+
#
|
|
9
|
+
# Response objects are returned by {WinterTc.fetch}. The raw body can be
|
|
10
|
+
# read as a plain string with {#text}, or parsed as JSON with {#json}.
|
|
11
|
+
#
|
|
12
|
+
# @example
|
|
13
|
+
# response = WinterTc.fetch("https://api.example.com/users")
|
|
14
|
+
# if response.ok
|
|
15
|
+
# users = response.json # => Array of user hashes
|
|
16
|
+
# else
|
|
17
|
+
# puts "Error: #{response.status} #{response.status_text}"
|
|
18
|
+
# end
|
|
19
|
+
class Response
|
|
20
|
+
# Mapping of common HTTP status codes to their standard reason phrases.
|
|
21
|
+
STATUS_TEXTS = {
|
|
22
|
+
100 => "Continue",
|
|
23
|
+
101 => "Switching Protocols",
|
|
24
|
+
102 => "Processing",
|
|
25
|
+
200 => "OK",
|
|
26
|
+
201 => "Created",
|
|
27
|
+
202 => "Accepted",
|
|
28
|
+
203 => "Non-Authoritative Information",
|
|
29
|
+
204 => "No Content",
|
|
30
|
+
205 => "Reset Content",
|
|
31
|
+
206 => "Partial Content",
|
|
32
|
+
301 => "Moved Permanently",
|
|
33
|
+
302 => "Found",
|
|
34
|
+
303 => "See Other",
|
|
35
|
+
304 => "Not Modified",
|
|
36
|
+
307 => "Temporary Redirect",
|
|
37
|
+
308 => "Permanent Redirect",
|
|
38
|
+
400 => "Bad Request",
|
|
39
|
+
401 => "Unauthorized",
|
|
40
|
+
402 => "Payment Required",
|
|
41
|
+
403 => "Forbidden",
|
|
42
|
+
404 => "Not Found",
|
|
43
|
+
405 => "Method Not Allowed",
|
|
44
|
+
406 => "Not Acceptable",
|
|
45
|
+
409 => "Conflict",
|
|
46
|
+
410 => "Gone",
|
|
47
|
+
411 => "Length Required",
|
|
48
|
+
413 => "Content Too Large",
|
|
49
|
+
415 => "Unsupported Media Type",
|
|
50
|
+
422 => "Unprocessable Content",
|
|
51
|
+
429 => "Too Many Requests",
|
|
52
|
+
500 => "Internal Server Error",
|
|
53
|
+
501 => "Not Implemented",
|
|
54
|
+
502 => "Bad Gateway",
|
|
55
|
+
503 => "Service Unavailable",
|
|
56
|
+
504 => "Gateway Timeout",
|
|
57
|
+
}.freeze
|
|
58
|
+
|
|
59
|
+
# The HTTP status code (e.g. +200+, +404+).
|
|
60
|
+
# @return [Integer]
|
|
61
|
+
attr_reader :status
|
|
62
|
+
|
|
63
|
+
# The response headers.
|
|
64
|
+
# @return [Headers]
|
|
65
|
+
attr_reader :headers
|
|
66
|
+
|
|
67
|
+
# The URL that produced this response (the final URL after any redirects).
|
|
68
|
+
# @return [String, nil]
|
|
69
|
+
attr_reader :url
|
|
70
|
+
|
|
71
|
+
# Creates a {Response} whose body is the JSON serialisation of +data+ and
|
|
72
|
+
# whose +Content-Type+ header is set to +application/json+. This mirrors
|
|
73
|
+
# the JavaScript
|
|
74
|
+
# {https://developer.mozilla.org/en-US/docs/Web/API/Response/json_static
|
|
75
|
+
# Response.json()} static method.
|
|
76
|
+
#
|
|
77
|
+
# @param data [Object] any JSON-serialisable value (Hash, Array, String, …)
|
|
78
|
+
# @param status [Integer] HTTP status code (default: +200+)
|
|
79
|
+
# @param headers [Hash, Headers, nil] additional response headers
|
|
80
|
+
# @return [Response]
|
|
81
|
+
#
|
|
82
|
+
# @example
|
|
83
|
+
# res = WinterTc::Response.json({ message: "hello" })
|
|
84
|
+
# res.status #=> 200
|
|
85
|
+
# res.headers.get("content-type") #=> "application/json"
|
|
86
|
+
# res.json #=> { "message" => "hello" }
|
|
87
|
+
def self.json(data, status: 200, headers: nil)
|
|
88
|
+
body = JSON.generate(data)
|
|
89
|
+
merged = Headers.new(headers)
|
|
90
|
+
merged.set("Content-Type", "application/json")
|
|
91
|
+
new(status: status, headers: merged, body: body)
|
|
92
|
+
end
|
|
93
|
+
|
|
94
|
+
# Creates a new Response.
|
|
95
|
+
#
|
|
96
|
+
# @param status [Integer] HTTP status code
|
|
97
|
+
# @param headers [Hash, Headers] response headers
|
|
98
|
+
# @param body [String, nil] raw response body
|
|
99
|
+
# @param url [String, nil] the URL that produced the response
|
|
100
|
+
def initialize(status:, headers:, body:, url: nil)
|
|
101
|
+
@status = Integer(status)
|
|
102
|
+
@headers = headers.is_a?(Headers) ? headers : Headers.new(headers)
|
|
103
|
+
@body = body.to_s
|
|
104
|
+
@url = url
|
|
105
|
+
end
|
|
106
|
+
|
|
107
|
+
# Returns +true+ if the HTTP status code indicates success (200–299).
|
|
108
|
+
#
|
|
109
|
+
# @return [Boolean]
|
|
110
|
+
def ok
|
|
111
|
+
@status >= 200 && @status < 300
|
|
112
|
+
end
|
|
113
|
+
|
|
114
|
+
# Alias for {#ok} for idiomatic Ruby usage.
|
|
115
|
+
alias ok? ok
|
|
116
|
+
|
|
117
|
+
# Returns the response body as a plain +String+.
|
|
118
|
+
#
|
|
119
|
+
# @return [String]
|
|
120
|
+
def text
|
|
121
|
+
@body
|
|
122
|
+
end
|
|
123
|
+
|
|
124
|
+
# Parses the response body as JSON and returns the result.
|
|
125
|
+
#
|
|
126
|
+
# @return [Object] the parsed JSON value (Hash, Array, String, etc.)
|
|
127
|
+
# @raise [JSON::ParserError] if the body is not valid JSON
|
|
128
|
+
def json
|
|
129
|
+
JSON.parse(@body)
|
|
130
|
+
end
|
|
131
|
+
|
|
132
|
+
# Returns the standard HTTP reason phrase for the {#status} code
|
|
133
|
+
# (e.g. +"OK"+ for 200, +"Not Found"+ for 404). Returns an empty
|
|
134
|
+
# string for unrecognised codes.
|
|
135
|
+
#
|
|
136
|
+
# @return [String]
|
|
137
|
+
def status_text
|
|
138
|
+
STATUS_TEXTS[@status] || ""
|
|
139
|
+
end
|
|
140
|
+
|
|
141
|
+
# @return [String]
|
|
142
|
+
def inspect
|
|
143
|
+
"#<#{self.class} #{@status} #{status_text}>"
|
|
144
|
+
end
|
|
145
|
+
end
|
|
146
|
+
end
|
data/lib/wintertc.rb
ADDED
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "wintertc/version"
|
|
4
|
+
require_relative "wintertc/error"
|
|
5
|
+
require_relative "wintertc/headers"
|
|
6
|
+
require_relative "wintertc/request"
|
|
7
|
+
require_relative "wintertc/response"
|
|
8
|
+
require_relative "wintertc/fetch"
|
|
9
|
+
|
|
10
|
+
# WinterTc is a Ruby HTTP client library that provides an interface as close as
|
|
11
|
+
# possible to the JavaScript
|
|
12
|
+
# {https://developer.mozilla.org/en-US/docs/Web/API/Fetch_API Fetch API}.
|
|
13
|
+
#
|
|
14
|
+
# The three primary building blocks mirror their JavaScript counterparts:
|
|
15
|
+
#
|
|
16
|
+
# * {WinterTc::Headers} — a case-insensitive map of HTTP header fields.
|
|
17
|
+
# * {WinterTc::Request} — an immutable description of an HTTP request.
|
|
18
|
+
# * {WinterTc::Response} — the server's response, with helpers for reading the
|
|
19
|
+
# body as text or JSON.
|
|
20
|
+
# * {WinterTc.fetch} — the main entry point; performs the request and returns a
|
|
21
|
+
# {WinterTc::Response}.
|
|
22
|
+
#
|
|
23
|
+
# The library has **no runtime dependencies** beyond Ruby's standard library
|
|
24
|
+
# (`net/http`, `uri`, `json`).
|
|
25
|
+
#
|
|
26
|
+
# @example Quick start
|
|
27
|
+
# require "wintertc"
|
|
28
|
+
#
|
|
29
|
+
# # Simple GET
|
|
30
|
+
# res = WinterTc.fetch("https://httpbin.org/get")
|
|
31
|
+
# puts res.status #=> 200
|
|
32
|
+
# puts res.ok #=> true
|
|
33
|
+
# puts res.json["url"] #=> "https://httpbin.org/get"
|
|
34
|
+
#
|
|
35
|
+
# # POST with JSON
|
|
36
|
+
# res = WinterTc.fetch(
|
|
37
|
+
# "https://httpbin.org/post",
|
|
38
|
+
# method: "POST",
|
|
39
|
+
# headers: { "Content-Type" => "application/json" },
|
|
40
|
+
# body: JSON.generate({ hello: "world" })
|
|
41
|
+
# )
|
|
42
|
+
# puts res.json["json"] #=> { "hello" => "world" }
|
|
43
|
+
module WinterTc
|
|
44
|
+
end
|
data/sig/wintertc.rbs
ADDED
|
@@ -0,0 +1,110 @@
|
|
|
1
|
+
module WinterTc
|
|
2
|
+
VERSION: String
|
|
3
|
+
MAX_REDIRECTS: Integer
|
|
4
|
+
|
|
5
|
+
# ---------------------------------------------------------------------------
|
|
6
|
+
# Error classes
|
|
7
|
+
# ---------------------------------------------------------------------------
|
|
8
|
+
|
|
9
|
+
class Error < StandardError
|
|
10
|
+
end
|
|
11
|
+
|
|
12
|
+
class TooManyRedirectsError < Error
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
class RedirectError < Error
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
class UnsupportedMethodError < Error
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
# ---------------------------------------------------------------------------
|
|
22
|
+
# Headers
|
|
23
|
+
# ---------------------------------------------------------------------------
|
|
24
|
+
|
|
25
|
+
# A case-insensitive map of HTTP header fields, mirroring the JavaScript
|
|
26
|
+
# Headers interface.
|
|
27
|
+
class Headers
|
|
28
|
+
include Enumerable[[String, String]]
|
|
29
|
+
|
|
30
|
+
def initialize: (?Hash[String, String] | Array[[String, String]] | Headers | nil) -> void
|
|
31
|
+
def get: (String name) -> String?
|
|
32
|
+
def set: (String name, String value) -> nil
|
|
33
|
+
def has: (String name) -> bool
|
|
34
|
+
def delete: (String name) -> nil
|
|
35
|
+
def append: (String name, String value) -> nil
|
|
36
|
+
def each: () { ([String, String]) -> void } -> self
|
|
37
|
+
def keys: () -> Array[String]
|
|
38
|
+
def values: () -> Array[String]
|
|
39
|
+
def to_h: () -> Hash[String, String]
|
|
40
|
+
def inspect: () -> String
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
# ---------------------------------------------------------------------------
|
|
44
|
+
# Request
|
|
45
|
+
# ---------------------------------------------------------------------------
|
|
46
|
+
|
|
47
|
+
# An immutable description of an HTTP request.
|
|
48
|
+
class Request
|
|
49
|
+
attr_reader url: String
|
|
50
|
+
attr_reader method: String
|
|
51
|
+
attr_reader headers: Headers
|
|
52
|
+
attr_reader body: String?
|
|
53
|
+
|
|
54
|
+
def initialize: (
|
|
55
|
+
String | Request input,
|
|
56
|
+
?method: String?,
|
|
57
|
+
?headers: Hash[String, String] | Headers | nil,
|
|
58
|
+
?body: String?
|
|
59
|
+
) -> void
|
|
60
|
+
|
|
61
|
+
def inspect: () -> String
|
|
62
|
+
end
|
|
63
|
+
|
|
64
|
+
# ---------------------------------------------------------------------------
|
|
65
|
+
# Response
|
|
66
|
+
# ---------------------------------------------------------------------------
|
|
67
|
+
|
|
68
|
+
# The server's HTTP response.
|
|
69
|
+
class Response
|
|
70
|
+
STATUS_TEXTS: Hash[Integer, String]
|
|
71
|
+
|
|
72
|
+
attr_reader status: Integer
|
|
73
|
+
attr_reader headers: Headers
|
|
74
|
+
attr_reader url: String?
|
|
75
|
+
|
|
76
|
+
def self.json: (
|
|
77
|
+
untyped data,
|
|
78
|
+
?status: Integer,
|
|
79
|
+
?headers: Hash[String, String] | Headers | nil
|
|
80
|
+
) -> Response
|
|
81
|
+
|
|
82
|
+
def initialize: (
|
|
83
|
+
status: Integer,
|
|
84
|
+
headers: Hash[String, String] | Headers,
|
|
85
|
+
body: String?,
|
|
86
|
+
?url: String?
|
|
87
|
+
) -> void
|
|
88
|
+
|
|
89
|
+
def ok: () -> bool
|
|
90
|
+
def ok?: () -> bool
|
|
91
|
+
def text: () -> String
|
|
92
|
+
def json: () -> untyped
|
|
93
|
+
def status_text: () -> String
|
|
94
|
+
def inspect: () -> String
|
|
95
|
+
end
|
|
96
|
+
|
|
97
|
+
# ---------------------------------------------------------------------------
|
|
98
|
+
# fetch
|
|
99
|
+
# ---------------------------------------------------------------------------
|
|
100
|
+
|
|
101
|
+
# Performs an HTTP request and returns the response. Synchronous.
|
|
102
|
+
def self.fetch: (
|
|
103
|
+
String | Request input,
|
|
104
|
+
?method: String?,
|
|
105
|
+
?headers: Hash[String, String] | Headers | nil,
|
|
106
|
+
?body: String?,
|
|
107
|
+
?redirect: :follow | :manual | :error,
|
|
108
|
+
**untyped
|
|
109
|
+
) -> Response
|
|
110
|
+
end
|
metadata
ADDED
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
|
2
|
+
name: wintertc
|
|
3
|
+
version: !ruby/object:Gem::Version
|
|
4
|
+
version: 0.1.0
|
|
5
|
+
platform: ruby
|
|
6
|
+
authors:
|
|
7
|
+
- kuboon
|
|
8
|
+
bindir: bin
|
|
9
|
+
cert_chain: []
|
|
10
|
+
date: 1980-01-02 00:00:00.000000000 Z
|
|
11
|
+
dependencies:
|
|
12
|
+
- !ruby/object:Gem::Dependency
|
|
13
|
+
name: minitest
|
|
14
|
+
requirement: !ruby/object:Gem::Requirement
|
|
15
|
+
requirements:
|
|
16
|
+
- - "~>"
|
|
17
|
+
- !ruby/object:Gem::Version
|
|
18
|
+
version: '5'
|
|
19
|
+
type: :development
|
|
20
|
+
prerelease: false
|
|
21
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
22
|
+
requirements:
|
|
23
|
+
- - "~>"
|
|
24
|
+
- !ruby/object:Gem::Version
|
|
25
|
+
version: '5'
|
|
26
|
+
- !ruby/object:Gem::Dependency
|
|
27
|
+
name: rake
|
|
28
|
+
requirement: !ruby/object:Gem::Requirement
|
|
29
|
+
requirements:
|
|
30
|
+
- - "~>"
|
|
31
|
+
- !ruby/object:Gem::Version
|
|
32
|
+
version: '13'
|
|
33
|
+
type: :development
|
|
34
|
+
prerelease: false
|
|
35
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
36
|
+
requirements:
|
|
37
|
+
- - "~>"
|
|
38
|
+
- !ruby/object:Gem::Version
|
|
39
|
+
version: '13'
|
|
40
|
+
description: |
|
|
41
|
+
WinterTc provides a Ruby HTTP client whose interface mirrors the JavaScript
|
|
42
|
+
Fetch API as closely as possible. It exposes WinterTc::Request,
|
|
43
|
+
WinterTc::Response, WinterTc::Headers, and WinterTc.fetch — all with the
|
|
44
|
+
same semantics as their browser counterparts. No runtime gem dependencies:
|
|
45
|
+
only Ruby's built-in net/http, uri, and json are used.
|
|
46
|
+
executables: []
|
|
47
|
+
extensions: []
|
|
48
|
+
extra_rdoc_files: []
|
|
49
|
+
files:
|
|
50
|
+
- CHANGELOG.md
|
|
51
|
+
- LICENSE
|
|
52
|
+
- README.md
|
|
53
|
+
- lib/wintertc.rb
|
|
54
|
+
- lib/wintertc/error.rb
|
|
55
|
+
- lib/wintertc/fetch.rb
|
|
56
|
+
- lib/wintertc/headers.rb
|
|
57
|
+
- lib/wintertc/request.rb
|
|
58
|
+
- lib/wintertc/response.rb
|
|
59
|
+
- lib/wintertc/version.rb
|
|
60
|
+
- sig/wintertc.rbs
|
|
61
|
+
homepage: https://github.com/kuboon/wintertc.rb
|
|
62
|
+
licenses:
|
|
63
|
+
- MIT
|
|
64
|
+
metadata:
|
|
65
|
+
homepage_uri: https://github.com/kuboon/wintertc.rb
|
|
66
|
+
source_code_uri: https://github.com/kuboon/wintertc.rb
|
|
67
|
+
changelog_uri: https://github.com/kuboon/wintertc.rb/blob/main/CHANGELOG.md
|
|
68
|
+
rdoc_options: []
|
|
69
|
+
require_paths:
|
|
70
|
+
- lib
|
|
71
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
|
72
|
+
requirements:
|
|
73
|
+
- - ">="
|
|
74
|
+
- !ruby/object:Gem::Version
|
|
75
|
+
version: '3.3'
|
|
76
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
|
77
|
+
requirements:
|
|
78
|
+
- - ">="
|
|
79
|
+
- !ruby/object:Gem::Version
|
|
80
|
+
version: '0'
|
|
81
|
+
requirements: []
|
|
82
|
+
rubygems_version: 4.0.3
|
|
83
|
+
specification_version: 4
|
|
84
|
+
summary: JavaScript-like Fetch API for Ruby
|
|
85
|
+
test_files: []
|