async-rest 0.5.2 → 0.6.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/.editorconfig +6 -0
- data/README.md +38 -12
- data/lib/async/rest/representation.rb +110 -0
- data/lib/async/rest/resource.rb +27 -34
- data/lib/async/rest/version.rb +1 -1
- data/lib/async/rest/wrapper/json.rb +10 -5
- metadata +5 -4
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: b348ab4bc1a06560f1f075791ddc9859f7b9148ea9df6befebc7bb228caee779
|
4
|
+
data.tar.gz: 596bad25fb27370bceb0eef0825a3af51ac34dcf27ff97a84e345aeb77d7aac0
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: d6998bb2e970995d1e09033ad118097cd4dcfb5af7b0cd95bafb6c0e205649cccc4ad84eef951b797ce5b7e59a3328e0269ae660b8d9e4ce70f4cbc0abc04223
|
7
|
+
data.tar.gz: ae37524edc67a1ee70c3cb884e9b4bbfc0000a0d24dc90f572965e2e3e9a22be334c71ff6ce5a2b4fc048a6d26cfc15a8c7b8a12a5bc5b5970fd442c7e1ba5a3
|
data/.editorconfig
ADDED
data/README.md
CHANGED
@@ -1,6 +1,11 @@
|
|
1
1
|
# Async::REST
|
2
2
|
|
3
|
-
|
3
|
+
Roy Thomas Fielding's thesis [Architectural Styles and the Design of Network-based Software Architectures](https://www.ics.uci.edu/~fielding/pubs/dissertation/top.htm) describes [Representational State Transfer](https://www.ics.uci.edu/~fielding/pubs/dissertation/rest_arch_style.htm) which comprises several core concepts:
|
4
|
+
|
5
|
+
- `Resource`: A conceptual mapping to one or more entities.
|
6
|
+
- `Representation`: An instance of a resource at a given point in time.
|
7
|
+
|
8
|
+
This gem models these abstractions as closely and practically as possible and serves as a basis for building asynchronous web clients.
|
4
9
|
|
5
10
|
[![Build Status](https://secure.travis-ci.org/socketry/async-rest.svg)](http://travis-ci.org/socketry/async-rest)
|
6
11
|
[![Code Climate](https://codeclimate.com/github/socketry/async-rest.svg)](https://codeclimate.com/github/socketry/async-rest)
|
@@ -28,22 +33,43 @@ Or install it yourself as:
|
|
28
33
|
|
29
34
|
## Usage
|
30
35
|
|
31
|
-
|
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.
|
37
|
+
|
38
|
+
### DNS over HTTP
|
39
|
+
|
40
|
+
This simple example shows how to use a custom representation to access DNS over HTTP.
|
32
41
|
|
33
42
|
```ruby
|
34
|
-
require 'async'
|
43
|
+
require 'async/http/server'
|
44
|
+
require 'async/http/url_endpoint'
|
45
|
+
|
35
46
|
require 'async/rest/resource'
|
47
|
+
require 'async/rest/representation'
|
48
|
+
|
49
|
+
module DNS
|
50
|
+
class Query < Async::REST::Representation
|
51
|
+
def initialize(*args)
|
52
|
+
# This is the old/weird content-type used by Google's DNS resolver. It's obsolete.
|
53
|
+
super(*args, wrapper: Async::REST::Wrapper::JSON.new("application/x-javascript"))
|
54
|
+
end
|
55
|
+
|
56
|
+
def question
|
57
|
+
value[:Question]
|
58
|
+
end
|
59
|
+
|
60
|
+
def answer
|
61
|
+
value[:Answer]
|
62
|
+
end
|
63
|
+
end
|
64
|
+
end
|
36
65
|
|
37
|
-
|
66
|
+
URL = 'https://dns.google.com/resolve'
|
67
|
+
Async::REST::Resource.for(URL) do |resource|
|
68
|
+
# Specify the representation class as the first argument (client side negotiation):
|
69
|
+
query = resource.get(DNS::Query, name: 'example.com', type: 'AAAA')
|
38
70
|
|
39
|
-
|
40
|
-
|
41
|
-
|
42
|
-
response = api.with(path: "users").get(page: 2)
|
43
|
-
|
44
|
-
pp response.read
|
45
|
-
|
46
|
-
api.close
|
71
|
+
pp query.metadata
|
72
|
+
pp query.value
|
47
73
|
end
|
48
74
|
```
|
49
75
|
|
@@ -0,0 +1,110 @@
|
|
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 'resource'
|
22
|
+
require_relative 'wrapper/json'
|
23
|
+
|
24
|
+
module Async
|
25
|
+
module REST
|
26
|
+
class RequestFailure < StandardError
|
27
|
+
end
|
28
|
+
|
29
|
+
# REST components perform actions on a resource by using a representation to capture the current or intended state of that resource and transferring that representation between components. A representation is a sequence of bytes, plus representation metadata to describe those bytes. Other commonly used but less precise names for a representation include: document, file, and HTTP message entity, instance, or variant.
|
30
|
+
#
|
31
|
+
# 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.
|
32
|
+
class Representation
|
33
|
+
def self.for(*args)
|
34
|
+
self.new(Resource.for(*args))
|
35
|
+
end
|
36
|
+
|
37
|
+
# @param resource [Resource] the RESTful resource that this representation is of.
|
38
|
+
# @param metadata [Hash | HTTP::Headers] the metadata associated wtih teh representation.
|
39
|
+
# @param value [Object] the value of the representation.
|
40
|
+
# @param wrapper [#prepare_request, #process_response] the wrapper for encoding/decoding the request/response body.
|
41
|
+
def initialize(resource, metadata: {}, value: nil, wrapper: Wrapper::JSON.new)
|
42
|
+
@resource = resource
|
43
|
+
@wrapper = wrapper
|
44
|
+
|
45
|
+
@metadata = metadata
|
46
|
+
@value = value
|
47
|
+
end
|
48
|
+
|
49
|
+
def with(**parameters)
|
50
|
+
self.class.new(@resource.with(parameters: parameters), wrapper: @wrapper)
|
51
|
+
end
|
52
|
+
|
53
|
+
def close
|
54
|
+
@resource.close
|
55
|
+
end
|
56
|
+
|
57
|
+
attr :resource
|
58
|
+
attr :wrapper
|
59
|
+
|
60
|
+
def prepare_request(verb, payload)
|
61
|
+
@resource.prepare_request(verb, payload, &@wrapper.method(:prepare_request))
|
62
|
+
end
|
63
|
+
|
64
|
+
def process_response(request, response)
|
65
|
+
@wrapper.process_response(request, response)
|
66
|
+
end
|
67
|
+
|
68
|
+
HTTP::VERBS.each do |verb|
|
69
|
+
# TODO when Ruby 3.0 lands, convert this to |payload = nil, **parameters|
|
70
|
+
# Blocked by https://bugs.ruby-lang.org/issues/14183
|
71
|
+
define_method(verb.downcase) do |payload = nil|
|
72
|
+
request = prepare_request(verb, payload)
|
73
|
+
|
74
|
+
response = @resource.call(request)
|
75
|
+
|
76
|
+
process_response(request, response)
|
77
|
+
end
|
78
|
+
end
|
79
|
+
|
80
|
+
attr :metadata
|
81
|
+
|
82
|
+
def value!
|
83
|
+
response = self.get
|
84
|
+
|
85
|
+
if response.success?
|
86
|
+
@metadata = response.headers
|
87
|
+
@value = response.read
|
88
|
+
else
|
89
|
+
raise RequestFailure, "Could not fetch remote resource #{@resource}: #{response.status}!"
|
90
|
+
end
|
91
|
+
end
|
92
|
+
|
93
|
+
def value
|
94
|
+
@value ||= value!
|
95
|
+
end
|
96
|
+
|
97
|
+
def value= value
|
98
|
+
if @value = value
|
99
|
+
self.post(value)
|
100
|
+
else
|
101
|
+
self.delete
|
102
|
+
end
|
103
|
+
end
|
104
|
+
|
105
|
+
def inspect
|
106
|
+
"\#<#{self.class} #{@resource.inspect}: value=#{@value.inspect}>"
|
107
|
+
end
|
108
|
+
end
|
109
|
+
end
|
110
|
+
end
|
data/lib/async/rest/resource.rb
CHANGED
@@ -18,8 +18,7 @@
|
|
18
18
|
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
19
19
|
# THE SOFTWARE.
|
20
20
|
|
21
|
-
|
22
|
-
|
21
|
+
require 'async'
|
23
22
|
require 'async/http/client'
|
24
23
|
require 'async/http/accept_encoding'
|
25
24
|
require 'async/http/reference'
|
@@ -27,17 +26,16 @@ require 'async/http/url_endpoint'
|
|
27
26
|
|
28
27
|
module Async
|
29
28
|
module REST
|
29
|
+
# The key abstraction of information in REST is a resource. Any information that can be named can be a resource: a document or image, a temporal service (e.g. "today's weather in Los Angeles"), a collection of other resources, a non-virtual object (e.g. a person), and so on. In other words, any concept that might be the target of an author's hypertext reference must fit within the definition of a resource. A resource is a conceptual mapping to a set of entities, not the entity that corresponds to the mapping at any particular point in time.
|
30
30
|
class Resource < HTTP::Middleware
|
31
31
|
# @param delegate [Async::HTTP::Middleware] the delegate that will handle requests.
|
32
|
-
# @param reference [Async::HTTP::Reference] the base request path/parameters.
|
32
|
+
# @param reference [Async::HTTP::Reference] the resource identifier (base request path/parameters).
|
33
33
|
# @param headers [Async::HTTP::Headers] the default headers that will be supplied with the request.
|
34
|
-
|
35
|
-
def initialize(delegate, reference = HTTP::Reference.parse, headers = HTTP::Headers.new, wrapper = Wrapper::JSON.new)
|
34
|
+
def initialize(delegate, reference = HTTP::Reference.parse, headers = HTTP::Headers.new)
|
36
35
|
super(delegate)
|
37
36
|
|
38
37
|
@reference = reference
|
39
38
|
@headers = headers
|
40
|
-
@wrapper = wrapper
|
41
39
|
end
|
42
40
|
|
43
41
|
def self.connect(url)
|
@@ -56,55 +54,50 @@ module Async
|
|
56
54
|
|
57
55
|
return resource unless block_given?
|
58
56
|
|
59
|
-
|
60
|
-
|
61
|
-
|
62
|
-
|
57
|
+
Async.run do
|
58
|
+
begin
|
59
|
+
yield resource
|
60
|
+
ensure
|
61
|
+
resource.close
|
62
|
+
end
|
63
63
|
end
|
64
64
|
end
|
65
65
|
|
66
66
|
attr :reference
|
67
67
|
attr :headers
|
68
|
-
attr :wrapper
|
69
68
|
|
70
|
-
def self.with(parent, *args, headers: {}, parameters: nil, path: nil
|
71
|
-
self.new(*args, parent.delegate, parent.reference.dup(path, parameters), parent.headers.merge(headers)
|
69
|
+
def self.with(parent, *args, headers: {}, parameters: nil, path: nil)
|
70
|
+
self.new(*args, parent.delegate, parent.reference.dup(path, parameters), parent.headers.merge(headers))
|
72
71
|
end
|
73
72
|
|
74
73
|
def with(*args, **options)
|
75
74
|
self.class.with(self, *args, **options)
|
76
75
|
end
|
77
76
|
|
78
|
-
def
|
79
|
-
|
80
|
-
|
81
|
-
|
82
|
-
|
83
|
-
|
84
|
-
|
85
|
-
headers = @headers.dup
|
86
|
-
|
77
|
+
def get(klass = Representation, **parameters)
|
78
|
+
klass.new(self.with(parameters: parameters)).tap(&:value)
|
79
|
+
end
|
80
|
+
|
81
|
+
# @param verb [String] the HTTP verb to use.
|
82
|
+
# @param payload [Object] the object which will used to generate the body of the request.
|
83
|
+
def prepare_request(verb, payload)
|
87
84
|
if payload
|
88
|
-
|
85
|
+
headers = @headers.dup
|
86
|
+
body = yield payload, headers
|
89
87
|
else
|
88
|
+
headers = @headers
|
90
89
|
body = nil
|
91
90
|
end
|
92
91
|
|
93
|
-
return HTTP::Request[verb, reference, headers, body]
|
92
|
+
return HTTP::Request[verb, @reference, headers, body]
|
94
93
|
end
|
95
94
|
|
96
|
-
def
|
97
|
-
@
|
95
|
+
def inspect
|
96
|
+
"\#<#{self.class} #{@reference.inspect} #{@headers.inspect}>"
|
98
97
|
end
|
99
98
|
|
100
|
-
|
101
|
-
|
102
|
-
request = prepare_request(verb, *args)
|
103
|
-
|
104
|
-
response = self.call(request)
|
105
|
-
|
106
|
-
process_response(response)
|
107
|
-
end
|
99
|
+
def to_s
|
100
|
+
"\#<#{self.class} #{@reference.to_s}>"
|
108
101
|
end
|
109
102
|
end
|
110
103
|
end
|
data/lib/async/rest/version.rb
CHANGED
@@ -44,17 +44,22 @@ module Async
|
|
44
44
|
if payload
|
45
45
|
headers['content-type'] = @content_type
|
46
46
|
|
47
|
+
# TODO dump incrementally to IO?
|
47
48
|
HTTP::Body::Buffered.new([
|
48
49
|
::JSON.dump(payload)
|
49
50
|
])
|
50
51
|
end
|
51
52
|
end
|
52
53
|
|
53
|
-
def process_response(response)
|
54
|
-
|
55
|
-
|
56
|
-
|
57
|
-
|
54
|
+
def process_response(request, response)
|
55
|
+
if content_type = response.headers['content-type']
|
56
|
+
if content_type.start_with? @content_type
|
57
|
+
if body = response.body
|
58
|
+
response.body = Parser.new(body)
|
59
|
+
end
|
60
|
+
else
|
61
|
+
warn "Unknown content type: #{content_type}!"
|
62
|
+
end
|
58
63
|
end
|
59
64
|
|
60
65
|
return response
|
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.
|
4
|
+
version: 0.6.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-
|
11
|
+
date: 2018-12-28 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
14
|
name: async-http
|
@@ -87,6 +87,7 @@ executables: []
|
|
87
87
|
extensions: []
|
88
88
|
extra_rdoc_files: []
|
89
89
|
files:
|
90
|
+
- ".editorconfig"
|
90
91
|
- ".gitignore"
|
91
92
|
- ".rspec"
|
92
93
|
- ".travis.yml"
|
@@ -95,6 +96,7 @@ files:
|
|
95
96
|
- Rakefile
|
96
97
|
- async-rest.gemspec
|
97
98
|
- lib/async/rest.rb
|
99
|
+
- lib/async/rest/representation.rb
|
98
100
|
- lib/async/rest/resource.rb
|
99
101
|
- lib/async/rest/version.rb
|
100
102
|
- lib/async/rest/wrapper/json.rb
|
@@ -116,8 +118,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
|
|
116
118
|
- !ruby/object:Gem::Version
|
117
119
|
version: '0'
|
118
120
|
requirements: []
|
119
|
-
|
120
|
-
rubygems_version: 2.7.7
|
121
|
+
rubygems_version: 3.0.1
|
121
122
|
signing_key:
|
122
123
|
specification_version: 4
|
123
124
|
summary: A library for RESTful clients (and hopefully servers).
|