resource_set 1.0.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +7 -0
- data/.gitignore +22 -0
- data/.rspec +2 -0
- data/.travis.yml +12 -0
- data/CHANGELOG.md +11 -0
- data/Gemfile +2 -0
- data/LICENSE.txt +22 -0
- data/README.md +165 -0
- data/Rakefile +5 -0
- data/examples/digitalocean_droplets.rb +60 -0
- data/examples/httpbin_client.rb +15 -0
- data/lib/resource_set/action.rb +66 -0
- data/lib/resource_set/action_invoker.rb +58 -0
- data/lib/resource_set/endpoint_resolver.rb +46 -0
- data/lib/resource_set/inheritable_attribute.rb +20 -0
- data/lib/resource_set/method_factory.rb +20 -0
- data/lib/resource_set/resource.rb +40 -0
- data/lib/resource_set/resource_collection.rb +55 -0
- data/lib/resource_set/status_code_mapper.rb +59 -0
- data/lib/resource_set/testing/action_handler_matchers.rb +42 -0
- data/lib/resource_set/testing/have_action_matchers.rb +85 -0
- data/lib/resource_set/testing.rb +20 -0
- data/lib/resource_set/version.rb +3 -0
- data/lib/resource_set.rb +17 -0
- data/resource_set.gemspec +30 -0
- data/spec/integration/resource_actions_spec.rb +41 -0
- data/spec/lib/resource_set/action_invoker_spec.rb +167 -0
- data/spec/lib/resource_set/action_spec.rb +87 -0
- data/spec/lib/resource_set/endpoint_resolver_spec.rb +60 -0
- data/spec/lib/resource_set/inheritable_attribute_spec.rb +54 -0
- data/spec/lib/resource_set/method_factory_spec.rb +50 -0
- data/spec/lib/resource_set/resource_collection_spec.rb +67 -0
- data/spec/lib/resource_set/resource_spec.rb +66 -0
- data/spec/lib/resource_set/status_code_mapper_spec.rb +9 -0
- data/spec/lib/resource_set/testing/action_handler_matchers_spec.rb +68 -0
- data/spec/lib/resource_set/testing/have_action_matchers_spec.rb +157 -0
- data/spec/spec_helper.rb +8 -0
- metadata +202 -0
checksums.yaml
ADDED
@@ -0,0 +1,7 @@
|
|
1
|
+
---
|
2
|
+
SHA1:
|
3
|
+
metadata.gz: 79aa7ef9a5449e0663ad6ec7d0bd3b038d46beda
|
4
|
+
data.tar.gz: 5a54cdfa3b831b445bbcfd422deec1547a34bc8c
|
5
|
+
SHA512:
|
6
|
+
metadata.gz: 38309396161c178f16b9c45c0720d2d65f928cd0aeb719e71d4491cecbda381b4bc92afe5be60a85be1abf1ddd093b8d9f7e57ff87028c75421cfea9fcb69290
|
7
|
+
data.tar.gz: a39ce7e9f13d7e1de224aad0eee5b9824c14866c22c706ac4a5957f1183c16f78ff16c43debde781a5c7dd9a19c02b217cf9868cd08a9388384e2bae4811974f
|
data/.gitignore
ADDED
@@ -0,0 +1,22 @@
|
|
1
|
+
*.gem
|
2
|
+
*.rbc
|
3
|
+
.bundle
|
4
|
+
.config
|
5
|
+
.yardoc
|
6
|
+
Gemfile.lock
|
7
|
+
InstalledFiles
|
8
|
+
_yardoc
|
9
|
+
coverage
|
10
|
+
doc/
|
11
|
+
lib/bundler/man
|
12
|
+
pkg
|
13
|
+
rdoc
|
14
|
+
spec/reports
|
15
|
+
test/tmp
|
16
|
+
test/version_tmp
|
17
|
+
tmp
|
18
|
+
*.bundle
|
19
|
+
*.so
|
20
|
+
*.o
|
21
|
+
*.a
|
22
|
+
mkmf.log
|
data/.rspec
ADDED
data/.travis.yml
ADDED
data/CHANGELOG.md
ADDED
@@ -0,0 +1,11 @@
|
|
1
|
+
Resource Set Changelog
|
2
|
+
======================
|
3
|
+
|
4
|
+
### master
|
5
|
+
|
6
|
+
### [v1.0.0][v1.0.0] (September 8, 2017)
|
7
|
+
|
8
|
+
* Fixed Fixnum deprecation warnings
|
9
|
+
([#4](https://github.com/kyrylo/resource_set/pull/4))
|
10
|
+
|
11
|
+
[v1.0.0]: https://github.com/kyrylo/resource_set/releases/tag/v1.0.0
|
data/Gemfile
ADDED
data/LICENSE.txt
ADDED
@@ -0,0 +1,22 @@
|
|
1
|
+
Copyright (c) 2014 Robert Ross
|
2
|
+
|
3
|
+
MIT License
|
4
|
+
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining
|
6
|
+
a copy of this software and associated documentation files (the
|
7
|
+
"Software"), to deal in the Software without restriction, including
|
8
|
+
without limitation the rights to use, copy, modify, merge, publish,
|
9
|
+
distribute, sublicense, and/or sell copies of the Software, and to
|
10
|
+
permit persons to whom the Software is furnished to do so, subject to
|
11
|
+
the following conditions:
|
12
|
+
|
13
|
+
The above copyright notice and this permission notice shall be
|
14
|
+
included in all copies or substantial portions of the Software.
|
15
|
+
|
16
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
17
|
+
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
|
18
|
+
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
|
19
|
+
NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
|
20
|
+
LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
|
21
|
+
OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
|
22
|
+
WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
data/README.md
ADDED
@@ -0,0 +1,165 @@
|
|
1
|
+
# Resource Set
|
2
|
+
|
3
|
+
Resource Set provides tools to aid in making API Clients. Such as URL resolving, Request / Response layer, and more.
|
4
|
+
|
5
|
+
## Installation
|
6
|
+
|
7
|
+
Add this line to your application's Gemfile:
|
8
|
+
|
9
|
+
gem 'resource_set'
|
10
|
+
|
11
|
+
And then execute:
|
12
|
+
|
13
|
+
$ bundle
|
14
|
+
|
15
|
+
Or install it yourself as:
|
16
|
+
|
17
|
+
$ gem install resource_set
|
18
|
+
|
19
|
+
## Usage
|
20
|
+
|
21
|
+
This library recommends using [Cartograph](http://github.com/kyrylo/cartograph) for representing and deserializing response bodies.
|
22
|
+
You'll see it in the examples provided below.
|
23
|
+
|
24
|
+
### Resource classes
|
25
|
+
|
26
|
+
Resource Set provides a comprehensive but intuitive DSL where you describe the remote resources capabilities.
|
27
|
+
For example, where can I get a list of users? Where do I get a single user? How do I create a user?
|
28
|
+
|
29
|
+
When you're able to answer these questions, you can describe them in your resource class like this:
|
30
|
+
|
31
|
+
```ruby
|
32
|
+
class DropletResource < ResourceSet::Resource
|
33
|
+
resources do
|
34
|
+
default_handler(422) { |response| ErrorMapping.extract_single(response.body, :read) }
|
35
|
+
default_handler(:ok, :created) { |response| DropletMapping.extract_single(response.body, :read) }
|
36
|
+
default_handler { |response| raise "Unexpected response status #{response.status}... #{response.body}" }
|
37
|
+
|
38
|
+
# Defining actions will create instance methods on the resource class to call them.
|
39
|
+
action :find do
|
40
|
+
verb :get # get is assumed if this is omitted
|
41
|
+
path '/droplets/:id'
|
42
|
+
handler(200) { |response| DropletMapping.extract_single(response.body, :read) }
|
43
|
+
end
|
44
|
+
|
45
|
+
action :all do
|
46
|
+
path '/droplets'
|
47
|
+
handler(200) { |body| DropletMapping.extract_collection(body, :read) }
|
48
|
+
end
|
49
|
+
|
50
|
+
action :create do
|
51
|
+
path '/droplets'
|
52
|
+
verb :post
|
53
|
+
body { |object| DropletMapping.representation_for(:create, object) } # Generate a response body from a passed object
|
54
|
+
handler(202) { |response| DropletMapping.extract_single(response.body, :read) }
|
55
|
+
end
|
56
|
+
end
|
57
|
+
end
|
58
|
+
```
|
59
|
+
|
60
|
+
You also have the option to use a shorter version to describe actions like this:
|
61
|
+
|
62
|
+
```ruby
|
63
|
+
class DropletResource < ResourceSet::Resource
|
64
|
+
resources do
|
65
|
+
action :all, 'GET /v2/droplets' do
|
66
|
+
handler(:ok) { |response| DropletMapping.extract_collection(response.body, :read) }
|
67
|
+
end
|
68
|
+
end
|
69
|
+
end
|
70
|
+
```
|
71
|
+
|
72
|
+
Instead of using `#action`, you can use any of the supported HTTP verb methods including `#get`, `#post`, `#put`, `#delete`, `#head`, `#patch`, and `#options`. Thus, the above example can be also written as:
|
73
|
+
|
74
|
+
```ruby
|
75
|
+
class DropletResource < ResourceSet::Resource
|
76
|
+
resources do
|
77
|
+
get :all, '/v2/droplets' do
|
78
|
+
handler(:ok) { |response| DropletMapping.extract_collection(response.body, :read) }
|
79
|
+
end
|
80
|
+
end
|
81
|
+
end
|
82
|
+
```
|
83
|
+
|
84
|
+
Now that we've described our resources. We can instantiate our class with a connection object. ResourceSet relies on the interface that Faraday provides. For example:
|
85
|
+
|
86
|
+
```ruby
|
87
|
+
conn = Faraday.new(url: 'http://api.digitalocean.com') do |req|
|
88
|
+
req.adapter :net_http
|
89
|
+
end
|
90
|
+
|
91
|
+
resource = DropletResource.new(connection: conn)
|
92
|
+
```
|
93
|
+
|
94
|
+
Now that we've instantiated a resource with our class, we can call the actions we've defined on it.
|
95
|
+
|
96
|
+
```
|
97
|
+
all_droplets = resource.all
|
98
|
+
single_droplet = resource.find(id: 123)
|
99
|
+
create = resource.create(Droplet.new)
|
100
|
+
```
|
101
|
+
|
102
|
+
## Scope
|
103
|
+
|
104
|
+
ResourceSet classes give you the option to pass in an optional scope object, so that you may interact with the resource with it that way.
|
105
|
+
|
106
|
+
For example, you may want to use this for nested resources:
|
107
|
+
|
108
|
+
```ruby
|
109
|
+
class CommentResource < ResourceSet::Resource
|
110
|
+
resources do
|
111
|
+
action :all do
|
112
|
+
path { "/users/#{user_id}/comments" }
|
113
|
+
handler(200) { |resp| CommentMapping.extract_collection(resp.body, :read) }
|
114
|
+
end
|
115
|
+
end
|
116
|
+
|
117
|
+
def user_id
|
118
|
+
scope.user_id
|
119
|
+
end
|
120
|
+
end
|
121
|
+
|
122
|
+
user = User.find(123)
|
123
|
+
resource = CommentResource.new(connection: conn, scope: user)
|
124
|
+
comments = resource.all #=> Will fetch from /users/123/comments
|
125
|
+
```
|
126
|
+
|
127
|
+
## Test Helpers
|
128
|
+
|
129
|
+
ResourceSet supplys test helpers that assist in certain things you'd want your resource classes to do.
|
130
|
+
|
131
|
+
Make sure you:
|
132
|
+
|
133
|
+
require 'resource_set/testing'
|
134
|
+
|
135
|
+
Testing a certain action:
|
136
|
+
|
137
|
+
```ruby
|
138
|
+
# Tag the spec with resource_set to bring in the helpers
|
139
|
+
RSpec.describe MyResourceClass, resource_set: true do
|
140
|
+
it 'has an all action' do
|
141
|
+
expect(MyResourceClass).to have_action(:all).that_handles(:ok, :no_content).at_path('/users')
|
142
|
+
end
|
143
|
+
|
144
|
+
it 'handles a 201 with response body' do
|
145
|
+
expect(MyResourceClass).to handle_response(:create).with(status: 201, body: '{"users":[]}') do |handled|
|
146
|
+
expect(handled).to all(be_kind_of(User))
|
147
|
+
end
|
148
|
+
end
|
149
|
+
end
|
150
|
+
```
|
151
|
+
|
152
|
+
### Nice to have's
|
153
|
+
|
154
|
+
Things we've thought about but just haven't implemented are:
|
155
|
+
|
156
|
+
* Pagination capabilities
|
157
|
+
|
158
|
+
|
159
|
+
## Contributing
|
160
|
+
|
161
|
+
1. Fork it ( https://github.com/kyrylo/resource_set/fork )
|
162
|
+
2. Create your feature branch (`git checkout -b my-new-feature`)
|
163
|
+
3. Commit your changes (`git commit -am 'Add some feature'`)
|
164
|
+
4. Push to the branch (`git push origin my-new-feature`)
|
165
|
+
5. Create a new Pull Request
|
data/Rakefile
ADDED
@@ -0,0 +1,60 @@
|
|
1
|
+
require 'kartograph'
|
2
|
+
|
3
|
+
class Droplet
|
4
|
+
attr_accessor :id, :name, :region, :size, :image
|
5
|
+
end
|
6
|
+
|
7
|
+
class DropletMapping
|
8
|
+
include Kartograph::DSL
|
9
|
+
|
10
|
+
kartograph do
|
11
|
+
mapping Droplet
|
12
|
+
root_key plural: 'droplets', singular: 'droplet', scopes: [:read]
|
13
|
+
|
14
|
+
property :id, scopes: [:read]
|
15
|
+
property :name, scopes: [:read, :create]
|
16
|
+
property :size, scopes: [:read, :create]
|
17
|
+
property :image, scopes: [:read, :create]
|
18
|
+
property :region, scopes: [:read, :create]
|
19
|
+
end
|
20
|
+
end
|
21
|
+
|
22
|
+
class DropletResource < ResourceSet::Resource
|
23
|
+
resources do
|
24
|
+
action :all do
|
25
|
+
verb :get
|
26
|
+
path '/v2/droplets'
|
27
|
+
handler(200) { |response| DropletMapping.extract_collection(response.body, :read) }
|
28
|
+
end
|
29
|
+
|
30
|
+
action :find do
|
31
|
+
verb :get
|
32
|
+
path '/v2/droplets/:id'
|
33
|
+
handler(200) { |response| DropletMapping.extract_single(response.body, :read) }
|
34
|
+
end
|
35
|
+
|
36
|
+
action :create do
|
37
|
+
verb :post
|
38
|
+
path '/v2/droplets'
|
39
|
+
body { |object| DropletMapping.representation_for(:create, object) }
|
40
|
+
handler(202) { |response| DropletMapping.extract_single(response.body, :read) }
|
41
|
+
end
|
42
|
+
|
43
|
+
action :update do
|
44
|
+
verb :put
|
45
|
+
path '/v2/droplets/123'
|
46
|
+
body { |object| DropletMapping.representation_for(:create, object) }
|
47
|
+
handler(200) { |response, object| DropletMapping.extract_into_object(object, response.body, :read) }
|
48
|
+
end
|
49
|
+
end
|
50
|
+
end
|
51
|
+
|
52
|
+
token = 'YOUR_ACCESS_TOKEN'
|
53
|
+
conn = Faraday.new(url: 'https://api.digitalocean.com', headers: { content_type: 'application/json', authorization: "Bearer #{token}" }) do |req|
|
54
|
+
req.adapter :net_http
|
55
|
+
end
|
56
|
+
|
57
|
+
resource = DropletResource.new(connection: conn)
|
58
|
+
|
59
|
+
# Retrieve all droplets
|
60
|
+
puts resource.all
|
@@ -0,0 +1,15 @@
|
|
1
|
+
require 'resource_set'
|
2
|
+
|
3
|
+
class HTTPBinResource < ResourceSet::Resource
|
4
|
+
resources do
|
5
|
+
get '/ip' => :ip
|
6
|
+
get '/status/:code' => :status
|
7
|
+
end
|
8
|
+
end
|
9
|
+
|
10
|
+
conn = Faraday.new(url: 'http://httpbin.org')
|
11
|
+
resource = HTTPBinResource.new(connection: conn)
|
12
|
+
|
13
|
+
puts resource.ip
|
14
|
+
puts
|
15
|
+
puts resource.status(code: 418)
|
@@ -0,0 +1,66 @@
|
|
1
|
+
module ResourceSet
|
2
|
+
class Action
|
3
|
+
attr_reader :name
|
4
|
+
|
5
|
+
def initialize(name, verb = nil, path = nil)
|
6
|
+
@name = name
|
7
|
+
@verb = (verb && verb.downcase.to_sym) || :get
|
8
|
+
@path = path
|
9
|
+
@query_keys = []
|
10
|
+
end
|
11
|
+
|
12
|
+
def verb(v = nil)
|
13
|
+
@verb = v if v
|
14
|
+
@verb
|
15
|
+
end
|
16
|
+
|
17
|
+
def path(path = nil, &block)
|
18
|
+
raise "You must pass either a block or a string for paths" if path and block_given?
|
19
|
+
@path = path if path
|
20
|
+
@path = block if block_given?
|
21
|
+
@path
|
22
|
+
end
|
23
|
+
|
24
|
+
def query_keys(*keys)
|
25
|
+
return @query_keys if keys.empty?
|
26
|
+
@query_keys += keys
|
27
|
+
end
|
28
|
+
|
29
|
+
def handlers
|
30
|
+
@handlers ||= {}
|
31
|
+
end
|
32
|
+
|
33
|
+
def handler(*response_codes, &block)
|
34
|
+
if response_codes.empty?
|
35
|
+
handlers[:any] = block
|
36
|
+
else
|
37
|
+
response_codes.each do |code|
|
38
|
+
code = StatusCodeMapper.code_for(code) unless code.is_a?(Integer)
|
39
|
+
handlers[code] = block
|
40
|
+
end
|
41
|
+
end
|
42
|
+
end
|
43
|
+
|
44
|
+
def body(&block)
|
45
|
+
@body_handler = block if block_given?
|
46
|
+
@body_handler
|
47
|
+
end
|
48
|
+
|
49
|
+
def hooks
|
50
|
+
@hooks ||= {}
|
51
|
+
end
|
52
|
+
|
53
|
+
def before_request(method_name = nil, &block)
|
54
|
+
hooks[:before] ||= []
|
55
|
+
|
56
|
+
if block_given?
|
57
|
+
hooks[:before] << block
|
58
|
+
else
|
59
|
+
raise "Must include a method name" unless method_name
|
60
|
+
hooks[:before] << method_name
|
61
|
+
end
|
62
|
+
|
63
|
+
nil
|
64
|
+
end
|
65
|
+
end
|
66
|
+
end
|
@@ -0,0 +1,58 @@
|
|
1
|
+
module ResourceSet
|
2
|
+
class ActionInvoker
|
3
|
+
attr_reader :action, :connection, :args, :options, :resource
|
4
|
+
|
5
|
+
def initialize(action, resource, *args)
|
6
|
+
@action = action
|
7
|
+
@resource = resource
|
8
|
+
@connection = resource.connection
|
9
|
+
@args = args
|
10
|
+
@options = args.last.kind_of?(Hash) ? args.last : {}
|
11
|
+
end
|
12
|
+
|
13
|
+
def self.call(action, resource, *args)
|
14
|
+
new(action, resource, *args).handle_response
|
15
|
+
end
|
16
|
+
|
17
|
+
def handle_response
|
18
|
+
if handler = action.handlers[response.status] || action.handlers[:any]
|
19
|
+
resource.instance_exec(response, *args, &handler) # Since the handler is a block, it does not enforce parameter length checking
|
20
|
+
else
|
21
|
+
response.body
|
22
|
+
end
|
23
|
+
end
|
24
|
+
|
25
|
+
def construct_body
|
26
|
+
action.body.call(*args[0..(action.body.arity - 1)])
|
27
|
+
end
|
28
|
+
|
29
|
+
def response
|
30
|
+
return @response if @response
|
31
|
+
|
32
|
+
raise ArgumentError, "Verb '#{action.verb}' is not allowed" unless ALLOWED_VERBS.include?(action.verb)
|
33
|
+
|
34
|
+
@response = connection.send(action.verb, resolver.resolve(options)) do |request|
|
35
|
+
request.body = construct_body if action.body and [:post, :put, :patch, :delete].include?(action.verb)
|
36
|
+
append_hooks(:before, request)
|
37
|
+
end
|
38
|
+
end
|
39
|
+
|
40
|
+
def resolver
|
41
|
+
path = action.path.kind_of?(Proc) ? resource.instance_eval(&action.path) : action.path
|
42
|
+
EndpointResolver.new(path: path, query_param_keys: action.query_keys)
|
43
|
+
end
|
44
|
+
|
45
|
+
private
|
46
|
+
|
47
|
+
def append_hooks(hook_type, request)
|
48
|
+
(action.hooks[hook_type] || []).each do |hook|
|
49
|
+
case hook
|
50
|
+
when Proc
|
51
|
+
resource.instance_exec(*args, request, &hook)
|
52
|
+
when Symbol
|
53
|
+
resource.send(hook, *args, request)
|
54
|
+
end
|
55
|
+
end
|
56
|
+
end
|
57
|
+
end
|
58
|
+
end
|