async-rest 0.10.1 → 0.12.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: 7532e90b101d5977772c3471fcbe2e9981677e2459e90f0c385c6ffab0c7c48c
4
- data.tar.gz: 36d2b5804bcfde6a3938752fc7b68f0b1af011fdfc6b9910d032431e76b90bb4
3
+ metadata.gz: bb987666810a3a57e687ffd7a3bdc84fa1d1202d9d175793c233d6fa7fd4f601
4
+ data.tar.gz: fd8512c49fc42b9c1d19be516f245f833731f293ebaed31656a6b7d29f1d6d44
5
5
  SHA512:
6
- metadata.gz: 6b9d994df5534243476a9fc4cb02d523f1a12ad12310ff4b637574543d37288f142b3909dfc01725a5ba5978abb0e0d1ea8d8767110c14a7a376c5e9f6a0a2dc
7
- data.tar.gz: fb8ac27b1b95768dbb89638a2f13c48865360e8f15b11df0991c712a0fdebc912d24d487e3191b8223952948b4c8a389fc1c794b01b32b74760e3294888e07ad
6
+ metadata.gz: 5e514f403513ee3886568b073ac2881af75de114b665d868874939aace962da6dcc8fefbb57f1fbcea3735e2705d3f7c17d82c25cfcb091f7b3356a56b45a9db
7
+ data.tar.gz: 2c08eb8becbabc19a0c1f6760722735f86a2fa659faf63c3fc6a4aabd998408aa9e0beb6bf0ecd8780dd6c8e94167d2678411baffd82040e352dfe88e6d6ace4
@@ -7,13 +7,15 @@ matrix:
7
7
  - rvm: 2.4
8
8
  - rvm: 2.5
9
9
  - rvm: 2.6
10
+ - rvm: 2.6
11
+ gemfile: gems/event.gemfile
12
+ - rvm: 2.6
13
+ env: COVERAGE=PartialSummary,Coveralls
10
14
  - rvm: jruby-head
11
15
  env: JRUBY_OPTS="--debug -X+O"
12
16
  - rvm: truffleruby
13
17
  - rvm: ruby-head
14
- - rvm: rbx-3
15
18
  allow_failures:
19
+ - rvm: truffleruby
16
20
  - rvm: ruby-head
17
21
  - rvm: jruby-head
18
- - rvm: rbx-3
19
- - rvm: truffleruby
data/README.md CHANGED
@@ -7,7 +7,7 @@ Roy Thomas Fielding's thesis [Architectural Styles and the Design of Network-bas
7
7
 
8
8
  This gem models these abstractions as closely and practically as possible and serves as a basis for building asynchronous web clients.
9
9
 
10
- [![Build Status](https://secure.travis-ci.org/socketry/async-rest.svg)](http://travis-ci.org/socketry/async-rest)
10
+ [![Build Status](https://secure.travis-ci.com/socketry/async-rest.svg)](http://travis-ci.com/socketry/async-rest)
11
11
  [![Code Climate](https://codeclimate.com/github/socketry/async-rest.svg)](https://codeclimate.com/github/socketry/async-rest)
12
12
  [![Coverage Status](https://coveralls.io/repos/socketry/async-rest/badge.svg)](https://coveralls.io/r/socketry/async-rest)
13
13
 
@@ -33,7 +33,7 @@ Or install it yourself as:
33
33
 
34
34
  ## Usage
35
35
 
36
- Generally speaking, you want to create a representation class for each endpoint. This class is responsible for negotiating content type and processing the response, and traversing related endpoints.
36
+ Generally speaking, you want to create a representation class for each remote resource. This class is responsible for negotiating content type and processing the response, and traversing related resources.
37
37
 
38
38
  ### DNS over HTTP
39
39
 
@@ -0,0 +1,111 @@
1
+ #!/usr/bin/env ruby
2
+
3
+ require 'async'
4
+ require 'async/rest'
5
+ require 'async/rest/wrapper/form'
6
+
7
+ require 'date'
8
+
9
+ URL = "https://api.github.com"
10
+ ENDPOINT = Async::HTTP::Endpoint.parse(URL)
11
+
12
+ module GitHub
13
+ class Wrapper < Async::REST::Wrapper::Form
14
+ DEFAULT_CONTENT_TYPES = {
15
+ "application/vnd.github.v3+json" => Async::REST::Wrapper::JSON::Parser
16
+ }
17
+
18
+ def initialize
19
+ super(DEFAULT_CONTENT_TYPES)
20
+ end
21
+
22
+ def parser_for(response)
23
+ if content_type = response.headers['content-type']
24
+ if content_type.start_with? "application/json"
25
+ return Async::REST::Wrapper::JSON::Parser
26
+ end
27
+ end
28
+
29
+ return super
30
+ end
31
+ end
32
+
33
+ class Representation < Async::REST::Representation[Wrapper]
34
+ end
35
+
36
+ class User < Representation
37
+ end
38
+
39
+ class Client < Representation
40
+ def user(name)
41
+ self.with(User, path: "users/#{name}")
42
+ end
43
+ end
44
+
45
+ module Paginate
46
+ include Enumerable
47
+
48
+ def represent(metadata, attributes)
49
+ resource = @resource.with(path: attributes[:id])
50
+
51
+ representation.new(resource, metadata: metadata, value: attributes)
52
+ end
53
+
54
+ def each(page: 1, per_page: 50, **parameters)
55
+ return to_enum(:each, page: page, per_page: per_page, **parameters) unless block_given?
56
+
57
+ while true
58
+ items = @resource.get(self.class, page: page, per_page: per_page, **parameters)
59
+
60
+ break if items.empty?
61
+
62
+ Array(items.value).each do |item|
63
+ yield represent(items.metadata, item)
64
+ end
65
+
66
+ page += 1
67
+
68
+ # Was this the last page?
69
+ break if items.value.size < per_page
70
+ end
71
+ end
72
+
73
+ def empty?
74
+ self.value.empty?
75
+ end
76
+ end
77
+
78
+ class Event < Representation
79
+ def created_at
80
+ DateTime.parse(value[:created_at])
81
+ end
82
+ end
83
+
84
+ class Events < Representation
85
+ include Paginate
86
+
87
+ def representation
88
+ Event
89
+ end
90
+ end
91
+
92
+ class User < Representation
93
+ def public_events
94
+ self.with(Events, path: "events/public")
95
+ end
96
+ end
97
+ end
98
+
99
+ puts "Connecting..."
100
+ headers = Protocol::HTTP::Headers.new
101
+ headers['user-agent'] = "async-rest/GitHub v#{Async::REST::VERSION}"
102
+
103
+ GitHub::Client.for(ENDPOINT, headers) do |client|
104
+ user = client.user("ioquatix")
105
+
106
+ events = user.public_events.to_a
107
+ pp events.first.created_at
108
+ pp events.last.created_at
109
+ end
110
+
111
+ puts "done"
@@ -0,0 +1,64 @@
1
+ #!/usr/bin/env ruby
2
+
3
+ require_relative '../../lib/async/rest'
4
+ require_relative '../../lib/async/rest/wrapper/url_encoded'
5
+
6
+ require 'nokogiri'
7
+
8
+ Async.logger.debug!
9
+
10
+ module XKCD
11
+ module Wrapper
12
+ # This defines how we interact with the XKCD service.
13
+ class HTML < Async::REST::Wrapper::URLEncoded
14
+ TEXT_HTML = "text/html"
15
+
16
+ # How to process the response body.
17
+ class Parser < ::Protocol::HTTP::Body::Wrapper
18
+ def join
19
+ Nokogiri::HTML(super)
20
+ end
21
+ end
22
+
23
+ # We wrap the response body with the parser (it could incrementally parse the body).
24
+ def wrap_response(response)
25
+ if body = response.body
26
+ response.body = Parser.new(body)
27
+ end
28
+ end
29
+
30
+ def process_response(request, response)
31
+ if content_type = response.headers['content-type']
32
+ if content_type.start_with? TEXT_HTML
33
+ wrap_response(response)
34
+ else
35
+ raise Error, "Unknown content type: #{content_type}!"
36
+ end
37
+ end
38
+
39
+ return response
40
+ end
41
+ end
42
+ end
43
+
44
+ # A comic representation.
45
+ class Comic < Async::REST::Representation[Wrapper::HTML]
46
+ def image_url
47
+ self.value.css("#comic img").attribute("src").text
48
+ end
49
+ end
50
+ end
51
+
52
+ Async do
53
+ URL = 'https://xkcd.com/'
54
+
55
+ Async::REST::Resource.for(URL) do |resource|
56
+ (2000..2010).each do |id|
57
+ Async do
58
+ representation = resource.with(path: "/#{id}/").get(XKCD::Comic)
59
+
60
+ p representation.image_url
61
+ end
62
+ end
63
+ end
64
+ end
@@ -23,7 +23,10 @@ module Async
23
23
  class Error < StandardError
24
24
  end
25
25
 
26
- class RequestFailure < Error
26
+ class RequestError < Error
27
+ end
28
+
29
+ class ResponseError < Error
27
30
  end
28
31
  end
29
32
  end
@@ -28,15 +28,35 @@ module Async
28
28
  #
29
29
  # A representation consists of data, metadata describing the data, and, on occasion, metadata to describe the metadata (usually for the purpose of verifying message integrity). Metadata is in the form of name-value pairs, where the name corresponds to a standard that defines the value's structure and semantics. Response messages may include both representation metadata and resource metadata: information about the resource that is not specific to the supplied representation.
30
30
  class Representation
31
+ def self.[] wrapper
32
+ klass = Class.new(Representation)
33
+
34
+ klass.const_set(:WRAPPER, wrapper)
35
+
36
+ return klass
37
+ end
38
+
31
39
  def self.for(*args, **options)
32
- self.new(Resource.for(*args), **options)
40
+ representation = self.new(Resource.for(*args), **options)
41
+
42
+ return representation unless block_given?
43
+
44
+ Async do
45
+ begin
46
+ yield representation
47
+ ensure
48
+ representation.close
49
+ end
50
+ end
33
51
  end
34
52
 
53
+ WRAPPER = Wrapper::JSON
54
+
35
55
  # @param resource [Resource] the RESTful resource that this representation is of.
36
56
  # @param metadata [Hash | HTTP::Headers] the metadata associated wtih teh representation.
37
57
  # @param value [Object] the value of the representation.
38
58
  # @param wrapper [#prepare_request, #process_response] the wrapper for encoding/decoding the request/response body.
39
- def initialize(resource, metadata: {}, value: nil, wrapper: Wrapper::JSON.new)
59
+ def initialize(resource, metadata: {}, value: nil, wrapper: self.class::WRAPPER.new)
40
60
  @resource = resource
41
61
  @wrapper = wrapper
42
62
 
@@ -63,6 +83,7 @@ module Async
63
83
  @resource.prepare_request(verb, payload, &@wrapper.method(:prepare_request))
64
84
  end
65
85
 
86
+ # If an exception propagates out of this method, the response will be closed.
66
87
  def process_response(request, response)
67
88
  @wrapper.process_response(request, response)
68
89
  end
@@ -20,6 +20,6 @@
20
20
 
21
21
  module Async
22
22
  module REST
23
- VERSION = "0.10.1"
23
+ VERSION = "0.12.0"
24
24
  end
25
25
  end
@@ -0,0 +1,61 @@
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
+ require_relative 'json'
22
+ require_relative 'url_encoded'
23
+
24
+ module Async
25
+ module REST
26
+ module Wrapper
27
+ class Form < Generic
28
+ DEFAULT_CONTENT_TYPES = {
29
+ JSON::APPLICATION_JSON => JSON::Parser,
30
+ URLEncoded::APPLICATION_FORM_URLENCODED => URLEncoded::Parser,
31
+ }
32
+
33
+ def initialize(content_types = DEFAULT_CONTENT_TYPES)
34
+ @content_types = content_types
35
+ end
36
+
37
+ def prepare_request(payload, headers)
38
+ headers['accept'] ||= @content_types.keys
39
+
40
+ if payload
41
+ headers['content-type'] = URLEncoded::APPLICATION_FORM_URLENCODED
42
+
43
+ ::Protocol::HTTP::Body::Buffered.new([
44
+ ::Protocol::HTTP::URL.encode(payload)
45
+ ])
46
+ end
47
+ end
48
+
49
+ def parser_for(response)
50
+ if content_type = response.headers['content-type']
51
+ if parser = @content_types[content_type]
52
+ return parser
53
+ end
54
+ end
55
+
56
+ return super
57
+ end
58
+ end
59
+ end
60
+ end
61
+ end
@@ -32,8 +32,27 @@ module Async
32
32
  # @param response [Protocol::HTTP::Response] the response that was received.
33
33
  # @return [Object] some application specific representation of the response.
34
34
  def process_response(request, response)
35
+ wrap_response(response)
36
+ end
37
+
38
+ def parser_for(response)
39
+ return Unsupported
40
+ end
41
+
42
+ # Wrap the response body in the given klass.
43
+ def wrap_response(response)
44
+ if body = response.body
45
+ response.body = parser_for(response).new(body)
46
+ end
47
+
35
48
  return response
36
49
  end
50
+
51
+ class Unsupported < HTTP::Body::Wrapper
52
+ def join
53
+ raise ResponseError, super
54
+ end
55
+ end
37
56
  end
38
57
  end
39
58
  end
@@ -48,7 +48,6 @@ module Async
48
48
  if payload
49
49
  headers['content-type'] = @content_type
50
50
 
51
- # TODO dump incrementally to IO?
52
51
  HTTP::Body::Buffered.new([
53
52
  ::JSON.dump(payload)
54
53
  ])
@@ -61,22 +60,14 @@ module Async
61
60
  end
62
61
  end
63
62
 
64
- def wrap_response(response)
65
- if body = response.body
66
- response.body = Parser.new(body)
67
- end
68
- end
69
-
70
- def process_response(request, response)
63
+ def parser_for(response)
71
64
  if content_type = response.headers['content-type']
72
65
  if content_type.start_with? @content_type
73
- wrap_response(response)
74
- else
75
- raise Error, "Unknown content type: #{content_type}!"
66
+ return Parser
76
67
  end
77
68
  end
78
69
 
79
- return response
70
+ return super
80
71
  end
81
72
  end
82
73
  end
@@ -59,22 +59,14 @@ module Async
59
59
  end
60
60
  end
61
61
 
62
- def wrap_response(response)
63
- if body = response.body
64
- response.body = Parser.new(body)
65
- end
66
- end
67
-
68
- def process_response(request, response)
62
+ def parser_for(response)
69
63
  if content_type = response.headers['content-type']
70
64
  if content_type.start_with? @content_type
71
- wrap_response(response)
72
- else
73
- raise Error, "Unknown content type: #{content_type}!"
65
+ return Parser
74
66
  end
75
67
  end
76
68
 
77
- return response
69
+ return super
78
70
  end
79
71
  end
80
72
  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.10.1
4
+ version: 0.12.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: 2019-08-13 00:00:00.000000000 Z
11
+ date: 2019-12-03 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: async-http
@@ -123,12 +123,15 @@ files:
123
123
  - README.md
124
124
  - Rakefile
125
125
  - async-rest.gemspec
126
+ - examples/github/feed.rb
126
127
  - examples/slack/clean.rb
128
+ - examples/xkcd/comic.rb
127
129
  - lib/async/rest.rb
128
130
  - lib/async/rest/error.rb
129
131
  - lib/async/rest/representation.rb
130
132
  - lib/async/rest/resource.rb
131
133
  - lib/async/rest/version.rb
134
+ - lib/async/rest/wrapper/form.rb
132
135
  - lib/async/rest/wrapper/generic.rb
133
136
  - lib/async/rest/wrapper/json.rb
134
137
  - lib/async/rest/wrapper/url_encoded.rb
@@ -150,7 +153,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
150
153
  - !ruby/object:Gem::Version
151
154
  version: '0'
152
155
  requirements: []
153
- rubygems_version: 3.0.3
156
+ rubygems_version: 3.0.6
154
157
  signing_key:
155
158
  specification_version: 4
156
159
  summary: A library for RESTful clients (and hopefully servers).