async-rest 0.2.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: 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