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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: a22ad3fd143a7cbe87a92c2653ab5c5aa5ed88fc2f3aba5154d1e2f6c365f7b6
4
- data.tar.gz: e89ffc8cd141a113ef605e6857288cf266fd2867e32eee179212c642c31d8dab
3
+ metadata.gz: b348ab4bc1a06560f1f075791ddc9859f7b9148ea9df6befebc7bb228caee779
4
+ data.tar.gz: 596bad25fb27370bceb0eef0825a3af51ac34dcf27ff97a84e345aeb77d7aac0
5
5
  SHA512:
6
- metadata.gz: 86b0c063038301619aacb0923a2ce50f5876c39ce5473e2109e22d19a3b8e6887090632c40e9d7f54c36869cc1a4a3fa0b26b779e4d0f135a81e58f8c805c2a6
7
- data.tar.gz: 9b97aa0f0efc50931ed535bf64eac34f43f7b380bbe916a404c2effc1409f2847d6907f3449949b9478b25c8f2184876463e44cf13ea8f376961dc5f00a15336
6
+ metadata.gz: d6998bb2e970995d1e09033ad118097cd4dcfb5af7b0cd95bafb6c0e205649cccc4ad84eef951b797ce5b7e59a3328e0269ae660b8d9e4ce70f4cbc0abc04223
7
+ data.tar.gz: ae37524edc67a1ee70c3cb884e9b4bbfc0000a0d24dc90f572965e2e3e9a22be334c71ff6ce5a2b4fc048a6d26cfc15a8c7b8a12a5bc5b5970fd442c7e1ba5a3
@@ -0,0 +1,6 @@
1
+ root = true
2
+
3
+ [*]
4
+ indent_style = tab
5
+ indent_size = 2
6
+
data/README.md CHANGED
@@ -1,6 +1,11 @@
1
1
  # Async::REST
2
2
 
3
- An asynchronous client and server implementation of RESTful interfaces.
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
- ### GET a remote resource
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
- API_URL = "https://reqres.in/api"
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
- Async.run do
40
- api = Async::REST::Resource.for(API_URL)
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
@@ -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
- require_relative 'wrapper/json'
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
- # @param wrapper [#prepare_request, #process_response] the wrapper for encoding/decoding the request/response body.
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
- begin
60
- yield resource
61
- ensure
62
- resource.close
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, wrapper: parent.wrapper)
71
- self.new(*args, parent.delegate, parent.reference.dup(path, parameters), parent.headers.merge(headers), wrapper)
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 prepare_request(verb, payload = nil, **parameters)
79
- if parameters.empty?
80
- reference = @reference
81
- else
82
- reference = @reference.dup(nil, parameters)
83
- end
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
- body = @wrapper.prepare_request(payload, headers)
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 process_response(response)
97
- @wrapper.process_response(response)
95
+ def inspect
96
+ "\#<#{self.class} #{@reference.inspect} #{@headers.inspect}>"
98
97
  end
99
98
 
100
- HTTP::VERBS.each do |verb|
101
- define_method(verb.downcase) do |*args|
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
@@ -20,6 +20,6 @@
20
20
 
21
21
  module Async
22
22
  module REST
23
- VERSION = "0.5.2"
23
+ VERSION = "0.6.0"
24
24
  end
25
25
  end
@@ -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
- # We expect all responses to be valid JSON.
55
- # I tried inspecting content-type, but too many servers do the wrong thing.
56
- if body = response.body
57
- response.body = Parser.new(body)
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.5.2
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-01 00:00:00.000000000 Z
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
- rubyforge_project:
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).