fetch-api 0.1.0 → 0.3.0

Sign up to get free protection for your applications and to get access to all the features.
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
  - - ">="