resource_set 1.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- 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
@@ -0,0 +1,46 @@
|
|
1
|
+
require 'addressable/uri'
|
2
|
+
|
3
|
+
module ResourceSet
|
4
|
+
class EndpointResolver
|
5
|
+
attr_reader :path, :query_param_keys
|
6
|
+
|
7
|
+
def initialize(options = {})
|
8
|
+
@path = options[:path]
|
9
|
+
@query_param_keys = options[:query_param_keys] || []
|
10
|
+
end
|
11
|
+
|
12
|
+
def resolve(values = {})
|
13
|
+
uri = Addressable::URI.parse(path)
|
14
|
+
new_path = generated_path(uri.path, values)
|
15
|
+
|
16
|
+
uri.path = normalized_path_components(new_path)
|
17
|
+
uri.query = append_query_values(uri, values) unless query_param_keys.empty?
|
18
|
+
|
19
|
+
uri.to_s
|
20
|
+
end
|
21
|
+
|
22
|
+
private
|
23
|
+
|
24
|
+
def generated_path(supplied_path, values)
|
25
|
+
values.inject(supplied_path) do |np, (key, value)|
|
26
|
+
np.gsub(":#{key}", value.to_s)
|
27
|
+
end
|
28
|
+
end
|
29
|
+
|
30
|
+
def normalized_path_components(*components)
|
31
|
+
components.reject(&:empty?).map do |piece|
|
32
|
+
# Remove leading and trailing forward slashes
|
33
|
+
piece.gsub(/(^\/)|(\/$)/, '')
|
34
|
+
end.join('/').insert(0, '/')
|
35
|
+
end
|
36
|
+
|
37
|
+
def append_query_values(uri, values)
|
38
|
+
pre_vals = uri.query_values || {}
|
39
|
+
params = query_param_keys.each_with_object(pre_vals) do |key, query_values|
|
40
|
+
query_values[key] = values[key] if values.has_key?(key)
|
41
|
+
end
|
42
|
+
|
43
|
+
URI.encode_www_form(params)
|
44
|
+
end
|
45
|
+
end
|
46
|
+
end
|
@@ -0,0 +1,20 @@
|
|
1
|
+
module ResourceSet
|
2
|
+
module InheritableAttribute
|
3
|
+
def inheritable_attr(name)
|
4
|
+
instance_eval <<-RUBY
|
5
|
+
def #{name}=(v)
|
6
|
+
@#{name} = v
|
7
|
+
end
|
8
|
+
|
9
|
+
def #{name}
|
10
|
+
@#{name} ||= InheritableAttribute.inherit(self, :#{name})
|
11
|
+
end
|
12
|
+
RUBY
|
13
|
+
end
|
14
|
+
|
15
|
+
def self.inherit(klass, name)
|
16
|
+
return unless klass.superclass.respond_to?(name) and value = klass.superclass.send(name)
|
17
|
+
value.clone
|
18
|
+
end
|
19
|
+
end
|
20
|
+
end
|
@@ -0,0 +1,20 @@
|
|
1
|
+
module ResourceSet
|
2
|
+
class MethodFactory
|
3
|
+
def self.construct(object, resource_collection, invoker = ActionInvoker)
|
4
|
+
resource_collection.each do |action|
|
5
|
+
if object.method_defined?(action.name)
|
6
|
+
raise ArgumentError, "Action '#{action.name}' is already defined on `#{object}`"
|
7
|
+
end
|
8
|
+
method_block = method_for_action(action, invoker)
|
9
|
+
|
10
|
+
object.send(:define_method, action.name, &method_block)
|
11
|
+
end
|
12
|
+
end
|
13
|
+
|
14
|
+
def self.method_for_action(action, invoker)
|
15
|
+
Proc.new do |*args|
|
16
|
+
invoker.call(action, self, *args)
|
17
|
+
end
|
18
|
+
end
|
19
|
+
end
|
20
|
+
end
|
@@ -0,0 +1,40 @@
|
|
1
|
+
require 'resource_set/inheritable_attribute'
|
2
|
+
|
3
|
+
module ResourceSet
|
4
|
+
class Resource
|
5
|
+
extend InheritableAttribute
|
6
|
+
inheritable_attr :_resources
|
7
|
+
|
8
|
+
attr_reader :connection, :scope
|
9
|
+
|
10
|
+
def initialize(connection: nil, scope: nil)
|
11
|
+
@connection = connection
|
12
|
+
@scope = scope
|
13
|
+
end
|
14
|
+
|
15
|
+
def self.resources(&block)
|
16
|
+
self._resources ||= ResourceCollection.new
|
17
|
+
|
18
|
+
if block_given?
|
19
|
+
self._resources.instance_eval(&block)
|
20
|
+
MethodFactory.construct(self, self._resources)
|
21
|
+
end
|
22
|
+
|
23
|
+
self._resources
|
24
|
+
end
|
25
|
+
|
26
|
+
def action(name)
|
27
|
+
_resources.find_action(name)
|
28
|
+
end
|
29
|
+
|
30
|
+
def action_and_connection(action_name)
|
31
|
+
ActionConnection.new(action(action_name), connection)
|
32
|
+
end
|
33
|
+
|
34
|
+
private
|
35
|
+
|
36
|
+
def _resources
|
37
|
+
self.class._resources
|
38
|
+
end
|
39
|
+
end
|
40
|
+
end
|
@@ -0,0 +1,55 @@
|
|
1
|
+
module ResourceSet
|
2
|
+
class ResourceCollection
|
3
|
+
extend Forwardable
|
4
|
+
def_delegators :@collection, :find, :<<, :each, :include?
|
5
|
+
|
6
|
+
def initialize
|
7
|
+
@collection = []
|
8
|
+
end
|
9
|
+
|
10
|
+
def action(name, verb_and_path = nil, &block)
|
11
|
+
action = Action.new(name, *parse_verb_and_path(verb_and_path))
|
12
|
+
action.handlers.merge!(default_handlers.dup)
|
13
|
+
action.instance_eval(&block) if block_given?
|
14
|
+
action.tap { |a| self << a }
|
15
|
+
end
|
16
|
+
|
17
|
+
ResourceSet::ALLOWED_VERBS.each do |verb|
|
18
|
+
define_method verb do |path_name, &block|
|
19
|
+
path, name = path_name.to_a.flatten
|
20
|
+
action(name, "#{verb.upcase} #{path}", &block)
|
21
|
+
end
|
22
|
+
end
|
23
|
+
|
24
|
+
def default_handler(*response_codes, &block)
|
25
|
+
if response_codes.empty?
|
26
|
+
default_handlers[:any] = block
|
27
|
+
else
|
28
|
+
response_codes.each do |code|
|
29
|
+
code = StatusCodeMapper.code_for(code) unless code.is_a?(Integer)
|
30
|
+
default_handlers[code] = block
|
31
|
+
end
|
32
|
+
end
|
33
|
+
end
|
34
|
+
|
35
|
+
def default_handlers
|
36
|
+
@default_handlers ||= {}
|
37
|
+
end
|
38
|
+
|
39
|
+
def find_action(name)
|
40
|
+
find do |action|
|
41
|
+
action.name == name
|
42
|
+
end
|
43
|
+
end
|
44
|
+
|
45
|
+
private
|
46
|
+
|
47
|
+
def parse_verb_and_path(verb_and_path)
|
48
|
+
return [] unless verb_and_path
|
49
|
+
regex = /(?<verb>GET|POST|HEAD|PUT|PATCH|DELETE|OPTIONS)?\s*(?<path>.+)?/i
|
50
|
+
matched = verb_and_path.match(regex)
|
51
|
+
|
52
|
+
return matched[:verb], matched[:path]
|
53
|
+
end
|
54
|
+
end
|
55
|
+
end
|
@@ -0,0 +1,59 @@
|
|
1
|
+
module ResourceSet
|
2
|
+
class StatusCodeMapper
|
3
|
+
MAP = {
|
4
|
+
continue: 100,
|
5
|
+
switching_protocols: 101,
|
6
|
+
processing: 102,
|
7
|
+
ok: 200,
|
8
|
+
created: 201,
|
9
|
+
accepted: 202,
|
10
|
+
non_authoritative_information: 203,
|
11
|
+
no_content: 204,
|
12
|
+
reset_content: 205,
|
13
|
+
partial_content: 206,
|
14
|
+
multi_status: 207,
|
15
|
+
im_used: 226,
|
16
|
+
multiple_choices: 300,
|
17
|
+
moved_permanently: 301,
|
18
|
+
found: 302,
|
19
|
+
see_other: 303,
|
20
|
+
not_modified: 304,
|
21
|
+
use_proxy: 305,
|
22
|
+
temporary_redirect: 307,
|
23
|
+
bad_request: 400,
|
24
|
+
unauthorized: 401,
|
25
|
+
payment_required: 402,
|
26
|
+
forbidden: 403,
|
27
|
+
not_found: 404,
|
28
|
+
method_not_allowed: 405,
|
29
|
+
not_acceptable: 406,
|
30
|
+
proxy_authentication_required: 407,
|
31
|
+
request_timeout: 408,
|
32
|
+
conflict: 409,
|
33
|
+
gone: 410,
|
34
|
+
length_required: 411,
|
35
|
+
precondition_failed: 412,
|
36
|
+
request_entity_too_large: 413,
|
37
|
+
request_uri_too_long: 414,
|
38
|
+
unsupported_media_type: 415,
|
39
|
+
requested_range_not_satisfiable: 416,
|
40
|
+
expectation_failed: 417,
|
41
|
+
unprocessable_entity: 422,
|
42
|
+
locked: 423,
|
43
|
+
failed_dependency: 424,
|
44
|
+
upgrade_required: 426,
|
45
|
+
internal_server_error: 500,
|
46
|
+
not_implemented: 501,
|
47
|
+
bad_gateway: 502,
|
48
|
+
service_unavailable: 503,
|
49
|
+
gateway_timeout: 504,
|
50
|
+
http_version_not_supported: 505,
|
51
|
+
insufficient_storage: 507,
|
52
|
+
not_extended: 510
|
53
|
+
}
|
54
|
+
|
55
|
+
def self.code_for(symbol)
|
56
|
+
MAP[symbol] || symbol
|
57
|
+
end
|
58
|
+
end
|
59
|
+
end
|
@@ -0,0 +1,42 @@
|
|
1
|
+
require 'ostruct'
|
2
|
+
|
3
|
+
module ResourceSet
|
4
|
+
module Testing
|
5
|
+
class ActionHandlerMatchers
|
6
|
+
ResponseStub = Class.new(OpenStruct)
|
7
|
+
|
8
|
+
attr_reader :action, :response_stub, :failure_message
|
9
|
+
|
10
|
+
def initialize(action)
|
11
|
+
@action = action
|
12
|
+
end
|
13
|
+
|
14
|
+
def with(options, &block)
|
15
|
+
@response_stub = ResponseStub.new(options)
|
16
|
+
@handled_block = block
|
17
|
+
|
18
|
+
self
|
19
|
+
end
|
20
|
+
|
21
|
+
def matches?(subject, &block)
|
22
|
+
@handled_block ||= block
|
23
|
+
action = subject.resources.find_action(self.action)
|
24
|
+
unless action
|
25
|
+
@failure_message = "expected :#{self.action} to be handled by the class."
|
26
|
+
return false
|
27
|
+
end
|
28
|
+
|
29
|
+
status_code = response_stub.status || 200
|
30
|
+
unless action.handlers[status_code]
|
31
|
+
@failure_message = "expected the #{status_code} status code to be handled by the class."
|
32
|
+
return false
|
33
|
+
end
|
34
|
+
|
35
|
+
handled_response = action.handlers[status_code].call(response_stub)
|
36
|
+
@handled_block.call(handled_response)
|
37
|
+
|
38
|
+
true
|
39
|
+
end
|
40
|
+
end
|
41
|
+
end
|
42
|
+
end
|
@@ -0,0 +1,85 @@
|
|
1
|
+
module ResourceSet
|
2
|
+
module Testing
|
3
|
+
class HaveActionMatchers
|
4
|
+
attr_reader :action, :path, :verb
|
5
|
+
|
6
|
+
def initialize(action)
|
7
|
+
@action = action
|
8
|
+
@failures = []
|
9
|
+
end
|
10
|
+
|
11
|
+
def that_handles(*codes)
|
12
|
+
codes.each do |code|
|
13
|
+
handler_codes << StatusCodeMapper.code_for(code)
|
14
|
+
end
|
15
|
+
|
16
|
+
self
|
17
|
+
end
|
18
|
+
|
19
|
+
def at_path(path)
|
20
|
+
@path = path
|
21
|
+
self
|
22
|
+
end
|
23
|
+
|
24
|
+
def with_verb(verb)
|
25
|
+
@verb = verb
|
26
|
+
self
|
27
|
+
end
|
28
|
+
|
29
|
+
def matches?(subject)
|
30
|
+
action = subject.resources.find_action(self.action)
|
31
|
+
return false unless action
|
32
|
+
|
33
|
+
%i(check_keys check_path check_verb).inject(true) do |rv, method_name|
|
34
|
+
break false unless send(method_name, action)
|
35
|
+
true
|
36
|
+
end
|
37
|
+
end
|
38
|
+
|
39
|
+
def handler_codes
|
40
|
+
@handler_codes ||= []
|
41
|
+
end
|
42
|
+
|
43
|
+
def failure_message
|
44
|
+
return "expected class to have action #{action}." if failures.empty?
|
45
|
+
|
46
|
+
failures.map do |validation, expected, got|
|
47
|
+
"expected #{expected} #{validation}, got #{got} instead."
|
48
|
+
end.join('\n')
|
49
|
+
end
|
50
|
+
|
51
|
+
private
|
52
|
+
attr_reader :failures
|
53
|
+
|
54
|
+
def check_keys(action)
|
55
|
+
keys = action.handlers.keys
|
56
|
+
|
57
|
+
unless handler_codes.empty?
|
58
|
+
handler_codes.each do |handler_code|
|
59
|
+
unless keys.include?(handler_code)
|
60
|
+
return failed(:status_code, handler_code, keys)
|
61
|
+
end
|
62
|
+
end
|
63
|
+
end
|
64
|
+
|
65
|
+
true
|
66
|
+
end
|
67
|
+
|
68
|
+
def check_path(action)
|
69
|
+
return true unless self.path
|
70
|
+
action.path == self.path or failed(:path, self.path, action.path)
|
71
|
+
end
|
72
|
+
|
73
|
+
def check_verb(action)
|
74
|
+
return true unless self.verb
|
75
|
+
checked_verb = self.verb.kind_of?(String) ? self.verb.downcase.to_sym : self.verb
|
76
|
+
action.verb == checked_verb or failed(:verb, checked_verb, action.verb)
|
77
|
+
end
|
78
|
+
|
79
|
+
def failed(validation, expected, got)
|
80
|
+
failures << [validation, expected, got]
|
81
|
+
false
|
82
|
+
end
|
83
|
+
end
|
84
|
+
end
|
85
|
+
end
|
@@ -0,0 +1,20 @@
|
|
1
|
+
require 'resource_set/testing/have_action_matchers'
|
2
|
+
require 'resource_set/testing/action_handler_matchers'
|
3
|
+
|
4
|
+
module ResourceSet
|
5
|
+
module Testing
|
6
|
+
def have_action(action)
|
7
|
+
HaveActionMatchers.new(action)
|
8
|
+
end
|
9
|
+
|
10
|
+
def handle_response(action, &block)
|
11
|
+
ActionHandlerMatchers.new(action)
|
12
|
+
end
|
13
|
+
end
|
14
|
+
end
|
15
|
+
|
16
|
+
if defined?(RSpec)
|
17
|
+
RSpec.configure do |config|
|
18
|
+
config.include ResourceSet::Testing, resource_set: true
|
19
|
+
end
|
20
|
+
end
|
data/lib/resource_set.rb
ADDED
@@ -0,0 +1,17 @@
|
|
1
|
+
require 'resource_set/version'
|
2
|
+
|
3
|
+
module ResourceSet
|
4
|
+
ALLOWED_VERBS = [:get, :post, :put, :delete, :head, :patch, :options]
|
5
|
+
ActionConnection = Struct.new(:action, :connection)
|
6
|
+
|
7
|
+
autoload :Resource, 'resource_set/resource'
|
8
|
+
autoload :ResourceCollection, 'resource_set/resource_collection'
|
9
|
+
|
10
|
+
autoload :Action, 'resource_set/action'
|
11
|
+
autoload :ActionInvoker, 'resource_set/action_invoker'
|
12
|
+
autoload :MethodFactory, 'resource_set/method_factory'
|
13
|
+
|
14
|
+
autoload :StatusCodeMapper, 'resource_set/status_code_mapper'
|
15
|
+
autoload :EndpointResolver, 'resource_set/endpoint_resolver'
|
16
|
+
|
17
|
+
end
|
@@ -0,0 +1,30 @@
|
|
1
|
+
# coding: utf-8
|
2
|
+
lib = File.expand_path('../lib', __FILE__)
|
3
|
+
$LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
|
4
|
+
require 'resource_set/version'
|
5
|
+
|
6
|
+
Gem::Specification.new do |spec|
|
7
|
+
spec.name = "resource_set"
|
8
|
+
spec.version = ResourceSet::VERSION
|
9
|
+
spec.authors = ["Robert Ross", "Ivan Vanderbyl", "Kyrylo Silin"]
|
10
|
+
spec.email = ["engineering@digitalocean.com", "rross@digitalocean.com", "ivan@digitalocean.com", "silin@kyrylo.org"]
|
11
|
+
spec.summary = %q{Resource Set provides tools to aid in making API Clients. Such as URL resolving, Request / Response layer, and more.}
|
12
|
+
spec.description = ''
|
13
|
+
spec.homepage = "https://github.com/kyrylo/resource_set"
|
14
|
+
spec.license = "MIT"
|
15
|
+
spec.required_ruby_version = '>= 2.0'
|
16
|
+
|
17
|
+
spec.files = `git ls-files -z`.split("\x0")
|
18
|
+
spec.executables = spec.files.grep(%r{^bin/}) { |f| File.basename(f) }
|
19
|
+
spec.test_files = spec.files.grep(%r{^(test|spec|features)/})
|
20
|
+
spec.require_paths = ["lib"]
|
21
|
+
|
22
|
+
spec.add_dependency 'addressable', '< 3.0.0', '>= 2.3.6'
|
23
|
+
|
24
|
+
spec.add_development_dependency 'rake', '~> 12.0'
|
25
|
+
spec.add_development_dependency 'faraday'
|
26
|
+
spec.add_development_dependency 'rspec', '~> 3.6'
|
27
|
+
spec.add_development_dependency 'webmock', '~> 3.0'
|
28
|
+
spec.add_development_dependency 'cartograph', '~> 1.0'
|
29
|
+
spec.add_development_dependency 'pry', '~> 0'
|
30
|
+
end
|