fetch-api 0.1.0 → 0.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: d8968e4c64a4d0b5370aef72a78f1b4ac04096057deb92d1bc1a6dd14353b3cd
4
- data.tar.gz: 2b7cdf82b75cf2c2d51abd5693f1ad278242e4e93645779b0909bfa7fd62de7a
3
+ metadata.gz: dc8cbe5804a5286df86b5840189dbbe3c1cfc77e68bfd33b0ba3244be99af5c2
4
+ data.tar.gz: f72c409553a6b531803d084d8e862c77329ef785579e9a11b5a461fadd481ca3
5
5
  SHA512:
6
- metadata.gz: 39bfda51fed25d97acb41128134615980b4eb35357a790d76c79feb1bf57c06e7f2047e2908c530d346e1b051f79b1846cf01c912209d1be6cebb7d1eeaf88c1
7
- data.tar.gz: 42cd5a712cfe067f16e70482c6013162d2a29d76666d709d5032badc79a4202a9c474d1ba38dd19598657ae1b2b0d51f5de32ef49ce7395d6221ba1f7d752526
6
+ metadata.gz: 6051e1551078686b6624604b23966804684f7a764ffb76c467e79cf5bfd09e0025c8dd315803b0e7c8b1fe83874e349984c1a3fa325306fb4c09d1f855524e0c
7
+ data.tar.gz: 6340aa6ca403e46a023753fd08144081009512a3b97c12a3b035ac58aa951e49e1218e7d543a52151d9600022ed40773f00c655926eaa5d59b7fc9c3f9fc8ca3
data/README.md CHANGED
@@ -1,4 +1,4 @@
1
- # Fetch API
1
+ # Fetch API for Ruby
2
2
 
3
3
  Ruby's Net::HTTP is very powerful, but has a complicated API. OpenURI is easy to use, but has limited functionality. Third-party HTTP clients each have different APIs, and it can sometimes be difficult to learn how to use them.
4
4
 
@@ -29,10 +29,12 @@ require 'fetch-api'
29
29
 
30
30
  res = Fetch::API.fetch('https://example.com')
31
31
 
32
- # res is a Rack::Response object
32
+ # res is a Fetch::Response object
33
33
  puts res.body
34
34
  ```
35
35
 
36
+ or
37
+
36
38
  ``` ruby
37
39
  require 'fetch-api'
38
40
 
@@ -41,6 +43,24 @@ include Fetch::API
41
43
  res = fetch('https://example.com')
42
44
  ```
43
45
 
46
+ Options for `fetch` method:
47
+
48
+ - `method`: HTTP method (default: `'GET'`)
49
+ - `headers`: Request headers (default: `{}`)
50
+ - `body`: Request body (default: `nil`)
51
+ - `redirect`: Follow redirects (one of `follow`, `error`, `manual`, default: `follow`)
52
+
53
+ Methods of `Fetch::Response` object:
54
+
55
+ - `body`: Response body (String)
56
+ - `headers`: Response headers
57
+ - `ok`: Whether the response was successful or not (status code is in the range 200-299)
58
+ - `redirected`: Whether the response is the result of a redirect
59
+ - `status`: Status code (e.g. `200`, `404`)
60
+ - `status_text`: Status text (e.g. `'OK'`, `'Not Found'`)
61
+ - `url`: Response URL
62
+ - `json(**args)`: An object that parses the response body as JSON. The arguments are passed to `JSON.parse`
63
+
44
64
  ### Post JSON
45
65
 
46
66
  ``` ruby
@@ -57,23 +77,30 @@ res = fetch('http://example.com', **{
57
77
  })
58
78
  ```
59
79
 
60
- ### Post Form
80
+ ### Post application/x-www-form-urlencoded
61
81
 
62
82
  ``` ruby
63
83
  res = fetch('http://example.com', **{
64
84
  method: 'POST',
65
85
 
66
- headers: {
67
- 'Content-Type' => 'multipart/form-data'
68
- },
69
-
70
- body: Rack::Multipart.build_multipart(
71
- file: Rack::Multipart::UploadedFile.new(io: StringIO.new('foo'), filename: 'foo.txt')
86
+ body: Fetch::URLSearchParams.new(
87
+ name: 'Alice'
72
88
  )
73
89
  })
74
90
  ```
75
91
 
76
- Note: `Rack::Multipart.build_multipart` returns nil if the parameter does not include UploadedFile.
92
+ ### Post multipart/form-data
93
+
94
+ ``` ruby
95
+ res = fetch('http://example.com', **{
96
+ method: 'POST',
97
+
98
+ body: Fetch::FormData.build(
99
+ name: 'Alice',
100
+ file: File.open('path/to/file.txt')
101
+ )
102
+ })
103
+ ```
77
104
 
78
105
  ## Development
79
106
 
data/lib/fetch/client.rb CHANGED
@@ -1,8 +1,11 @@
1
1
  require_relative '../fetch'
2
+ require_relative 'form_data'
3
+ require_relative 'headers'
4
+ require_relative 'response'
5
+ require_relative 'url_search_params'
2
6
 
7
+ require 'marcel'
3
8
  require 'net/http'
4
- require 'net/https'
5
- require 'rack/response'
6
9
  require 'singleton'
7
10
  require 'uri'
8
11
 
@@ -10,15 +13,35 @@ module Fetch
10
13
  class Client
11
14
  include Singleton
12
15
 
13
- def fetch(resource, method: 'GET', headers: {}, body: nil, redirect: 'follow')
16
+ def fetch(resource, method: 'GET', headers: [], body: nil, redirect: 'follow', _redirected: false)
14
17
  uri = URI.parse(resource)
15
18
  req = Net::HTTP.const_get(method.capitalize).new(uri)
16
19
 
20
+ headers = Headers.new(headers) unless headers.is_a?(Headers)
21
+
17
22
  headers.each do |k, v|
18
23
  req[k] = v
19
24
  end
20
25
 
21
- req.body = body
26
+ case body
27
+ when FormData
28
+ req.set_form body.entries.map {|k, v|
29
+ if v.is_a?(File)
30
+ [k, v, {
31
+ filename: File.basename(v.path),
32
+ content_type: Marcel::MimeType.for(v) || 'application/octet-stream'
33
+ }]
34
+ else
35
+ [k, v]
36
+ end
37
+ }, 'multipart/form-data'
38
+ when URLSearchParams
39
+ req['Content-Type'] ||= 'application/x-www-form-urlencoded'
40
+
41
+ req.body = body.to_s
42
+ else
43
+ req.body = body
44
+ end
22
45
 
23
46
  http = Net::HTTP.new(uri.hostname, uri.port)
24
47
  http.use_ssl = uri.scheme == 'https'
@@ -29,23 +52,37 @@ module Fetch
29
52
  when Net::HTTPRedirection
30
53
  case redirect
31
54
  when 'follow'
32
- fetch(res['location'], method:, headers:, body:, redirect:)
55
+ fetch(res['Location'], method:, headers:, body:, redirect:, _redirected: true)
33
56
  when 'error'
34
- raise RedirectError, "redirected to #{res['location']}"
57
+ raise RedirectError, "redirected to #{res['Location']}"
35
58
  when 'manual'
36
- to_rack_response(res)
59
+ to_response(resource, res, _redirected)
37
60
  else
38
61
  raise ArgumentError, "invalid redirect option: #{redirect}"
39
62
  end
40
63
  else
41
- to_rack_response(res)
64
+ to_response(resource, res, _redirected)
42
65
  end
43
66
  end
44
67
 
45
68
  private
46
69
 
47
- def to_rack_response(res)
48
- Rack::Response.new(res.body, res.code, res.to_hash)
70
+ def to_response(url, res, redirected)
71
+ headers = Headers.new
72
+
73
+ res.each do |k, vs|
74
+ vs.split(', ').each do |v|
75
+ headers.append k, v
76
+ end
77
+ end
78
+
79
+ Response.new(
80
+ url: ,
81
+ status: res.code.to_i,
82
+ headers: ,
83
+ body: res.body,
84
+ redirected:
85
+ )
49
86
  end
50
87
  end
51
88
  end
@@ -0,0 +1,60 @@
1
+ module Fetch
2
+ class FormData
3
+ include Enumerable
4
+
5
+ def self.build(enumerable)
6
+ data = FormData.new
7
+
8
+ enumerable.each do |k, v|
9
+ data.append k, v
10
+ end
11
+
12
+ data
13
+ end
14
+
15
+ def initialize
16
+ @entries = []
17
+ end
18
+
19
+ attr_reader :entries
20
+
21
+ def append(key, value)
22
+ @entries.push [key.to_s, value]
23
+ end
24
+
25
+ def delete(key)
26
+ @entries.reject! {|k,| k == key.to_s }
27
+ end
28
+
29
+ def get(key)
30
+ @entries.assoc(key.to_s)&.last
31
+ end
32
+
33
+ def get_all(key)
34
+ @entries.select {|k,| k == key.to_s }.map(&:last)
35
+ end
36
+
37
+ def has(key)
38
+ @entries.any? {|k,| k == key.to_s }
39
+ end
40
+
41
+ def keys
42
+ @entries.map(&:first)
43
+ end
44
+
45
+ def set(key, value)
46
+ delete key
47
+ append key, value
48
+ end
49
+
50
+ def values
51
+ @entries.map(&:last)
52
+ end
53
+
54
+ def each(&block)
55
+ return enum_for(:each) unless block_given?
56
+
57
+ @entries.each(&block)
58
+ end
59
+ end
60
+ end
@@ -0,0 +1,50 @@
1
+ module Fetch
2
+ class Headers
3
+ include Enumerable
4
+
5
+ def initialize(init = [])
6
+ @entries = []
7
+
8
+ init.each do |k, v|
9
+ append k, v
10
+ end
11
+ end
12
+
13
+ attr_reader :entries
14
+
15
+ def append(key, value)
16
+ @entries << [key.to_s.downcase, value]
17
+ end
18
+
19
+ def delete(key)
20
+ @entries.delete_if {|k,| k == key.to_s.downcase }
21
+ end
22
+
23
+ def get(key)
24
+ @entries.select {|k,| k == key.to_s.downcase }.map(&:last).join(', ')
25
+ end
26
+
27
+ def has(key)
28
+ @entries.any? {|k,| k == key.to_s.downcase }
29
+ end
30
+
31
+ def keys
32
+ @entries.map(&:first)
33
+ end
34
+
35
+ def set(key, value)
36
+ delete key
37
+ append key, value
38
+ end
39
+
40
+ def values
41
+ @entries.map(&:last)
42
+ end
43
+
44
+ def each(&block)
45
+ return enum_for(:each) unless block_given?
46
+
47
+ @entries.each(&block)
48
+ end
49
+ end
50
+ end
@@ -0,0 +1,18 @@
1
+ require 'json'
2
+ require 'rack/utils'
3
+
4
+ module Fetch
5
+ Response = Data.define(:url, :status, :headers, :body, :redirected) {
6
+ def ok
7
+ status.between?(200, 299)
8
+ end
9
+
10
+ def status_text
11
+ Rack::Utils::HTTP_STATUS_CODES[status]
12
+ end
13
+
14
+ def json(**opts)
15
+ JSON.parse(body, **opts)
16
+ end
17
+ }
18
+ end
@@ -0,0 +1,71 @@
1
+ require 'uri'
2
+
3
+ module Fetch
4
+ class URLSearchParams
5
+ include Enumerable
6
+
7
+ def initialize(options = [])
8
+ @entries = []
9
+
10
+ case options
11
+ when String
12
+ initialize(URI.decode_www_form(options.delete_prefix('?')))
13
+ when Enumerable
14
+ options.each do |k, v|
15
+ append k, v
16
+ end
17
+ else
18
+ raise ArgumentError
19
+ end
20
+ end
21
+
22
+ attr_reader :entries
23
+
24
+ def append(key, value)
25
+ @entries.push [key.to_s, value]
26
+ end
27
+
28
+ def delete(key)
29
+ @entries.reject! {|k,| k == key.to_s }
30
+ end
31
+
32
+ def get(key)
33
+ @entries.assoc(key.to_s)&.last
34
+ end
35
+
36
+ def get_all(key)
37
+ @entries.select {|k,| k == key.to_s }.map(&:last)
38
+ end
39
+
40
+ def has(key)
41
+ @entries.any? {|k,| k == key.to_s }
42
+ end
43
+
44
+ def keys
45
+ @entries.map(&:first)
46
+ end
47
+
48
+ def set(key, value)
49
+ delete key
50
+ append key, value
51
+ end
52
+
53
+ def sort
54
+ @entries.sort_by!(&:first)
55
+ end
56
+
57
+ def to_s
58
+ URI.encode_www_form(@entries)
59
+ end
60
+
61
+ def values
62
+ @entries.map(&:last)
63
+ end
64
+
65
+ def each(&block)
66
+ return enum_for(:each) unless block_given?
67
+
68
+ @entries.each(&block)
69
+ end
70
+ end
71
+ end
data/lib/fetch/version.rb CHANGED
@@ -1,3 +1,3 @@
1
1
  module Fetch
2
- VERSION = '0.1.0'
2
+ VERSION = '0.3.0'
3
3
  end
data/lib/fetch.rb CHANGED
@@ -1,5 +1,4 @@
1
1
  require_relative 'fetch/version'
2
- require_relative 'fetch/api'
3
2
 
4
3
  module Fetch
5
4
  class Error < StandardError; end
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: fetch-api
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.1.0
4
+ version: 0.3.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Keita Urashima
@@ -10,6 +10,20 @@ bindir: exe
10
10
  cert_chain: []
11
11
  date: 2024-07-06 00:00:00.000000000 Z
12
12
  dependencies:
13
+ - !ruby/object:Gem::Dependency
14
+ name: marcel
15
+ requirement: !ruby/object:Gem::Requirement
16
+ requirements:
17
+ - - ">="
18
+ - !ruby/object:Gem::Version
19
+ version: '0'
20
+ type: :runtime
21
+ prerelease: false
22
+ version_requirements: !ruby/object:Gem::Requirement
23
+ requirements:
24
+ - - ">="
25
+ - !ruby/object:Gem::Version
26
+ version: '0'
13
27
  - !ruby/object:Gem::Dependency
14
28
  name: net-http
15
29
  requirement: !ruby/object:Gem::Requirement
@@ -79,6 +93,10 @@ files:
79
93
  - lib/fetch.rb
80
94
  - lib/fetch/api.rb
81
95
  - lib/fetch/client.rb
96
+ - lib/fetch/form_data.rb
97
+ - lib/fetch/headers.rb
98
+ - lib/fetch/response.rb
99
+ - lib/fetch/url_search_params.rb
82
100
  - lib/fetch/version.rb
83
101
  - sig/fetch.rbs
84
102
  homepage: https://github.com/ursm/fetch-api
@@ -95,7 +113,7 @@ required_ruby_version: !ruby/object:Gem::Requirement
95
113
  requirements:
96
114
  - - ">="
97
115
  - !ruby/object:Gem::Version
98
- version: 3.1.0
116
+ version: 3.2.0
99
117
  required_rubygems_version: !ruby/object:Gem::Requirement
100
118
  requirements:
101
119
  - - ">="