async-rest 0.2.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: ce0c7cbe77ef56cba58f5cd57ad1486a9cf9040d70fea765c6300f51e5590ddb
4
- data.tar.gz: 167cf326a9f2195cc0482bd7223cfa62a0ea9bdabce42a20076801bbc72ff9ef
3
+ metadata.gz: 839be8ad4eee0471cff7053c05692ae11178f8ab934c2485c61c09b67cca6cf0
4
+ data.tar.gz: af6e5721af9c1585fcd03c3e95d886ad639b71b789972ac630ed62c4c7c9c264
5
5
  SHA512:
6
- metadata.gz: 6cc434bcd3df1f0c4659e189e2d710650bad4a4a954f483173228d6678fda02d97f95391f1c29bcd3b5610ac318464213570d3febb0a0eb560a58028a9ed0b3a
7
- data.tar.gz: e231949363b899e5cc5d7739c8a7dcab667e46fd2a4bea91f766cb7c1683d9c27379049e565b4f7016fa5b0149aa92250f96bcf25b792e976e7a2af2ab6dcc50
6
+ metadata.gz: 32ff875407f491ddf920a8ebe437ca91d7fc2d6bdaa343c61b40aa292a0ca380c7414c146ec16ce3d4125627eb8f21cf21f920ab110d9c23a32fc7c4a9d5e4bf
7
+ data.tar.gz: 29a11597c7af0851afeb8eef0d053abe854451e4e0c9d3dc41c10f8ea1b36d20a22bcf7261d676d3a1d3a9ef24476133e46e153d8f826a3b7ef134896e0d5b80
data/.travis.yml CHANGED
@@ -10,6 +10,7 @@ matrix:
10
10
  - rvm: 2.3
11
11
  - rvm: 2.4
12
12
  - rvm: 2.5
13
+ - rvm: 2.6
13
14
  - rvm: jruby-head
14
15
  env: JRUBY_OPTS="--debug -X+O"
15
16
  - rvm: ruby-head
data/Gemfile CHANGED
@@ -8,6 +8,5 @@ group :development do
8
8
  end
9
9
 
10
10
  group :test do
11
- gem 'simplecov'
12
- gem 'coveralls', require: false
11
+ gem 'covered', require: 'covered/rspec' if RUBY_VERSION >= "2.6.0"
13
12
  end
data/async-rest.gemspec CHANGED
@@ -7,7 +7,7 @@ Gem::Specification.new do |spec|
7
7
  spec.authors = ["Samuel Williams"]
8
8
  spec.email = ["samuel.williams@oriontransfer.co.nz"]
9
9
 
10
- spec.summary = "A library for RESTful clients and servers."
10
+ spec.summary = "A library for RESTful clients (and hopefully servers)."
11
11
  spec.homepage = "https://github.com/socketry/async-rest"
12
12
 
13
13
  spec.files = `git ls-files -z`.split("\x0").reject do |f|
@@ -16,7 +16,7 @@ Gem::Specification.new do |spec|
16
16
  spec.executables = spec.files.grep(%r{^bin/}) { |f| File.basename(f) }
17
17
  spec.require_paths = ["lib"]
18
18
 
19
- spec.add_dependency("async-http", "~> 0.18.0")
19
+ spec.add_dependency "async-http", "~> 0.27.12"
20
20
 
21
21
  spec.add_development_dependency "async-rspec", "~> 1.1"
22
22
 
data/lib/async/rest.rb CHANGED
@@ -18,4 +18,6 @@
18
18
  # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
19
19
  # THE SOFTWARE.
20
20
 
21
- require "async/rest/version"
21
+ require_relative 'rest/version'
22
+ require_relative 'rest/reference'
23
+ require_relative 'rest/resource'
@@ -18,37 +18,36 @@
18
18
  # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
19
19
  # THE SOFTWARE.
20
20
 
21
- require_relative 'reference'
22
- require_relative 'json_body'
21
+ require_relative 'wrapper/json'
23
22
 
24
23
  require 'async/http/client'
24
+ require 'async/http/accept_encoding'
25
+ require 'async/http/reference'
25
26
  require 'async/http/url_endpoint'
26
27
 
27
28
  module Async
28
29
  module REST
29
- class Resource
30
- DEFAULT_HEADERS = {
31
- 'accept-encoding' => 'gzip',
32
- 'accept' => 'application/json;q=0.9, */*;q=0.8'
33
- }
34
-
35
- def initialize(client, reference = Reference.parse, headers = DEFAULT_HEADERS, max_redirects: 10)
36
- @client = client
30
+ class Resource < HTTP::Middleware
31
+ def initialize(client, reference = HTTP::Reference.parse, headers = HTTP::Headers.new, wrapper = Wrapper::JSON.new)
32
+ super(client)
33
+
37
34
  @reference = reference
38
35
  @headers = headers
39
-
40
- @max_redirects = max_redirects
36
+ @wrapper = wrapper
41
37
  end
42
38
 
43
- def close
44
- @client.close
39
+ def self.connect(url)
40
+ endpoint = HTTP::URLEndpoint.parse(url)
41
+
42
+ reference = HTTP::Reference.parse(endpoint.url.request_uri)
43
+
44
+ return HTTP::AcceptEncoding.new(HTTP::Client.new(endpoint)), reference
45
45
  end
46
46
 
47
- def self.for(url, headers = {}, **options)
48
- endpoint = HTTP::URLEndpoint.parse(url)
49
- client = HTTP::Client.new(endpoint)
47
+ def self.for(url, *args)
48
+ client, reference = connect(url)
50
49
 
51
- resource = self.new(client, Reference.parse(endpoint.url.request_uri), headers)
50
+ resource = self.new(client, reference, *args)
52
51
 
53
52
  return resource unless block_given?
54
53
 
@@ -63,76 +62,35 @@ module Async
63
62
  attr :reference
64
63
  attr :headers
65
64
 
66
- attr :max_redirects
67
-
68
- def [] path
69
- self.class.new(@client, @reference.nest(path), @headers, max_redirects: @max_redirects)
65
+ def self.nest(parent, path = nil, *args)
66
+ self.new(*args, parent.client, parent.reference.dup(path), parent.headers, parent.wrapper)
70
67
  end
71
68
 
72
69
  def with(**headers)
73
- self.class.new(@client, @reference, @headers.merge(headers), max_redirects: @max_redirects)
74
- end
75
-
76
- def wrapper_for(content_type)
77
- if content_type == 'application/json'
78
- return JSONBody
79
- end
70
+ self.class.new(@client, @reference, @headers.merge(headers), @wrapper)
80
71
  end
81
72
 
82
- def prepare_body(payload)
83
- return [] if payload.nil?
73
+ def prepare_request(verb, payload = nil, headers = nil, **parameters)
74
+ headers ||= @headers.dup
75
+ reference = @reference.dup(nil, parameters)
76
+ body = @wrapper.prepare_request(payload, headers)
84
77
 
85
- content_type = @headers['content-type']
86
-
87
- if wrapper = wrapper_for(content_type)
88
- return wrapper.dump(payload)
89
- else
90
- raise ArgumentError.new("Unsure how to convert payload to #{content_type}!")
91
- end
78
+ return HTTP::Request[verb, reference, headers, body]
92
79
  end
93
80
 
94
81
  def process_response(response)
95
- response.body = HTTP::InflateBody.for_response(response)
96
-
97
- content_type = response.headers['content-type']
98
-
99
- if wrapper = wrapper_for(content_type)
100
- response.body = wrapper.new(response.body)
101
- end
102
-
103
- return response
82
+ @wrapper.process_response(response)
104
83
  end
105
84
 
106
- HTTP::Client::VERBS.each do |verb|
107
- define_method(verb.downcase) do |payload = nil, **parameters, &block|
108
- reference = @reference.dup(nil, parameters)
85
+ HTTP::VERBS.each do |verb|
86
+ define_method(verb.downcase) do |*args|
87
+ request = prepare_request(verb, *args)
109
88
 
110
- if body = prepare_body(payload)
111
- body = HTTP::DeflateBody.for_request(@headers, body)
112
- end
89
+ response = self.call(request)
113
90
 
114
- response = self.request(verb, reference.to_str, @headers, body) do |response|
115
- process_response(response)
116
- end
117
-
118
- return response
91
+ process_response(response)
119
92
  end
120
93
  end
121
-
122
- def request(verb, location, *args)
123
- @max_redirects.times do
124
- @client.request(verb, location, *args) do |response|
125
- if response.redirection?
126
- verb = 'GET' unless response.preserve_method?
127
- location = response.headers['location']
128
- else
129
- return yield response
130
- end
131
- end
132
- end
133
-
134
- raise ArgumentError.new("Too many redirections!")
135
- end
136
94
  end
137
95
  end
138
96
  end
@@ -20,6 +20,6 @@
20
20
 
21
21
  module Async
22
22
  module REST
23
- VERSION = "0.2.0"
23
+ VERSION = "0.3.0"
24
24
  end
25
25
  end
@@ -22,27 +22,45 @@ require 'json'
22
22
 
23
23
  module Async
24
24
  module REST
25
- class JSONBody
26
- def initialize(body)
27
- @body = body
28
- end
29
-
30
- def close
31
- @body = @body.close
25
+ module Wrapper
26
+ class JSON
27
+ CONTENT_TYPE = "application/json".freeze
32
28
 
33
- return self
34
- end
35
-
36
- def join
37
- JSON.parse(@body.join, symbolize_names: true)
38
- end
39
-
40
- def self.dump(payload)
41
- JSON.dump(payload)
42
- end
43
-
44
- def finished?
45
- @body.finished?
29
+ def initialize(content_type = CONTENT_TYPE)
30
+ @content_type = content_type
31
+ end
32
+
33
+ def split(*args)
34
+ @content_type.split
35
+ end
36
+
37
+ def prepare_request(payload, headers)
38
+ headers['accept'] ||= @content_type
39
+
40
+ if payload
41
+ headers['content-type'] = @content_type
42
+
43
+ HTTP::Body::Buffered.new([
44
+ ::JSON.dump(payload)
45
+ ])
46
+ end
47
+ end
48
+
49
+ def process_response(response)
50
+ # We expect all responses to be valid JSON.
51
+ # I tried inspecting content-type, but too many servers do the wrong thing.
52
+ if body = response.body
53
+ response.body = Parser.new(body)
54
+ end
55
+
56
+ return response
57
+ end
58
+
59
+ class Parser < HTTP::Body::Wrapper
60
+ def join
61
+ ::JSON.parse(super, symbolize_names: true)
62
+ end
63
+ end
46
64
  end
47
65
  end
48
66
  end
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: async-rest
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.2.0
4
+ version: 0.3.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Samuel Williams
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2018-04-14 00:00:00.000000000 Z
11
+ date: 2018-07-15 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: async-http
@@ -16,14 +16,14 @@ dependencies:
16
16
  requirements:
17
17
  - - "~>"
18
18
  - !ruby/object:Gem::Version
19
- version: 0.18.0
19
+ version: 0.27.12
20
20
  type: :runtime
21
21
  prerelease: false
22
22
  version_requirements: !ruby/object:Gem::Requirement
23
23
  requirements:
24
24
  - - "~>"
25
25
  - !ruby/object:Gem::Version
26
- version: 0.18.0
26
+ version: 0.27.12
27
27
  - !ruby/object:Gem::Dependency
28
28
  name: async-rspec
29
29
  requirement: !ruby/object:Gem::Requirement
@@ -95,10 +95,9 @@ files:
95
95
  - Rakefile
96
96
  - async-rest.gemspec
97
97
  - lib/async/rest.rb
98
- - lib/async/rest/json_body.rb
99
- - lib/async/rest/reference.rb
100
98
  - lib/async/rest/resource.rb
101
99
  - lib/async/rest/version.rb
100
+ - lib/async/rest/wrapper/json.rb
102
101
  homepage: https://github.com/socketry/async-rest
103
102
  licenses: []
104
103
  metadata: {}
@@ -121,5 +120,5 @@ rubyforge_project:
121
120
  rubygems_version: 2.7.6
122
121
  signing_key:
123
122
  specification_version: 4
124
- summary: A library for RESTful clients and servers.
123
+ summary: A library for RESTful clients (and hopefully servers).
125
124
  test_files: []
@@ -1,139 +0,0 @@
1
- # Copyright, 2018, by Samuel G. D. Williams. <http://www.codeotaku.com>
2
- #
3
- # Permission is hereby granted, free of charge, to any person obtaining a copy
4
- # of this software and associated documentation files (the "Software"), to deal
5
- # in the Software without restriction, including without limitation the rights
6
- # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
7
- # copies of the Software, and to permit persons to whom the Software is
8
- # furnished to do so, subject to the following conditions:
9
- #
10
- # The above copyright notice and this permission notice shall be included in
11
- # all copies or substantial portions of the Software.
12
- #
13
- # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
14
- # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
15
- # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
16
- # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
17
- # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
18
- # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
19
- # THE SOFTWARE.
20
-
21
- module Async
22
- module REST
23
- # A relative reference, excluding any authority.
24
- class Reference
25
- def initialize(path, query_string, fragment, parameters)
26
- @path = path
27
- @query_string = query_string
28
- @fragment = fragment
29
- @parameters = parameters
30
- end
31
-
32
- # Generate a reference from a path and user parameters. The path may contain a `#fragment` or `?query=parameters`.
33
- def self.parse(path = '/', parameters = nil)
34
- base, fragment = path.split('#', 2)
35
- path, query_string = base.split('?', 2)
36
-
37
- self.new(path, query_string, fragment, parameters)
38
- end
39
-
40
- # The path component, e.g. /foo/bar/index.html
41
- attr :path
42
-
43
- # The un-parsed query string, e.g. 'x=10&y=20'
44
- attr :query_string
45
-
46
- # A fragment, the part after the '#'
47
- attr :fragment
48
-
49
- # User supplied parameters that will be appended to the query part.
50
- attr :parameters
51
-
52
- def append(buffer)
53
- if @query_string
54
- buffer << escape_path(@path) << '?' << query_string
55
- buffer << '&' << encode(@parameters) if @parameters
56
- else
57
- buffer << escape_path(@path)
58
- buffer << '?' << encode(@parameters) if @parameters
59
- end
60
-
61
- if @fragment
62
- buffer << '#' << escape(@fragment)
63
- end
64
-
65
- return buffer
66
- end
67
-
68
- def to_str
69
- append(String.new)
70
- end
71
-
72
- alias to_s to_str
73
-
74
- def + path
75
- self.dup(path)
76
- end
77
-
78
- def [] parameters
79
- self.dup(nil, parameters)
80
- end
81
-
82
- def dup(path = nil, parameters = nil)
83
- if parameters and @parameters
84
- parameters = @parameters.merge(parameters)
85
- else
86
- parameters = @parameters
87
- end
88
-
89
- if path
90
- path = @path + '/' + path
91
- else
92
- path = @path
93
- end
94
-
95
- self.class.new(path, @query_string, @fragment, parameters)
96
- end
97
-
98
- private
99
-
100
- # Escapes a generic string, using percent encoding.
101
- def escape(string)
102
- encoding = string.encoding
103
- string.b.gsub(/([^a-zA-Z0-9_.\-]+)/) do |m|
104
- '%' + m.unpack('H2' * m.bytesize).join('%').upcase
105
- end.force_encoding(encoding)
106
- end
107
-
108
- # According to https://tools.ietf.org/html/rfc3986#section-3.3, we escape non-pchar.
109
- NON_PCHAR = /([^a-zA-Z0-9_\-\.~!$&'()*+,;=:@\/]+)/.freeze
110
-
111
- # Escapes a path
112
- def escape_path(path)
113
- encoding = path.encoding
114
- path.b.gsub(NON_PCHAR) do |m|
115
- '%' + m.unpack('H2' * m.bytesize).join('%').upcase
116
- end.force_encoding(encoding)
117
- end
118
-
119
- # Encodes a hash or array into a query string
120
- def encode(value, prefix = nil)
121
- case value
122
- when Array
123
- value.map { |v|
124
- encode(v, "#{prefix}[]")
125
- }.join("&")
126
- when Hash
127
- value.map { |k, v|
128
- encode(v, prefix ? "#{prefix}[#{escape(k.to_s)}]" : escape(k.to_s))
129
- }.reject(&:empty?).join('&')
130
- when nil
131
- prefix
132
- else
133
- raise ArgumentError, "value must be a Hash" if prefix.nil?
134
- "#{prefix}=#{escape(value.to_s)}"
135
- end
136
- end
137
- end
138
- end
139
- end