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.
Files changed (38) hide show
  1. checksums.yaml +7 -0
  2. data/.gitignore +22 -0
  3. data/.rspec +2 -0
  4. data/.travis.yml +12 -0
  5. data/CHANGELOG.md +11 -0
  6. data/Gemfile +2 -0
  7. data/LICENSE.txt +22 -0
  8. data/README.md +165 -0
  9. data/Rakefile +5 -0
  10. data/examples/digitalocean_droplets.rb +60 -0
  11. data/examples/httpbin_client.rb +15 -0
  12. data/lib/resource_set/action.rb +66 -0
  13. data/lib/resource_set/action_invoker.rb +58 -0
  14. data/lib/resource_set/endpoint_resolver.rb +46 -0
  15. data/lib/resource_set/inheritable_attribute.rb +20 -0
  16. data/lib/resource_set/method_factory.rb +20 -0
  17. data/lib/resource_set/resource.rb +40 -0
  18. data/lib/resource_set/resource_collection.rb +55 -0
  19. data/lib/resource_set/status_code_mapper.rb +59 -0
  20. data/lib/resource_set/testing/action_handler_matchers.rb +42 -0
  21. data/lib/resource_set/testing/have_action_matchers.rb +85 -0
  22. data/lib/resource_set/testing.rb +20 -0
  23. data/lib/resource_set/version.rb +3 -0
  24. data/lib/resource_set.rb +17 -0
  25. data/resource_set.gemspec +30 -0
  26. data/spec/integration/resource_actions_spec.rb +41 -0
  27. data/spec/lib/resource_set/action_invoker_spec.rb +167 -0
  28. data/spec/lib/resource_set/action_spec.rb +87 -0
  29. data/spec/lib/resource_set/endpoint_resolver_spec.rb +60 -0
  30. data/spec/lib/resource_set/inheritable_attribute_spec.rb +54 -0
  31. data/spec/lib/resource_set/method_factory_spec.rb +50 -0
  32. data/spec/lib/resource_set/resource_collection_spec.rb +67 -0
  33. data/spec/lib/resource_set/resource_spec.rb +66 -0
  34. data/spec/lib/resource_set/status_code_mapper_spec.rb +9 -0
  35. data/spec/lib/resource_set/testing/action_handler_matchers_spec.rb +68 -0
  36. data/spec/lib/resource_set/testing/have_action_matchers_spec.rb +157 -0
  37. data/spec/spec_helper.rb +8 -0
  38. 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
@@ -0,0 +1,3 @@
1
+ module ResourceSet
2
+ VERSION = '1.0.0'
3
+ end
@@ -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