acfs 1.3.3 → 1.6.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (96) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +372 -0
  3. data/LICENSE +22 -0
  4. data/README.md +321 -0
  5. data/acfs.gemspec +38 -0
  6. data/lib/acfs.rb +51 -0
  7. data/lib/acfs/adapter/base.rb +26 -0
  8. data/lib/acfs/adapter/typhoeus.rb +82 -0
  9. data/lib/acfs/collection.rb +28 -0
  10. data/lib/acfs/collections/paginatable.rb +76 -0
  11. data/lib/acfs/configuration.rb +120 -0
  12. data/lib/acfs/errors.rb +147 -0
  13. data/lib/acfs/global.rb +101 -0
  14. data/lib/acfs/location.rb +76 -0
  15. data/lib/acfs/middleware/base.rb +24 -0
  16. data/lib/acfs/middleware/json.rb +31 -0
  17. data/lib/acfs/middleware/logger.rb +23 -0
  18. data/lib/acfs/middleware/msgpack.rb +32 -0
  19. data/lib/acfs/middleware/print.rb +23 -0
  20. data/lib/acfs/middleware/serializer.rb +41 -0
  21. data/lib/acfs/operation.rb +96 -0
  22. data/lib/acfs/request.rb +32 -0
  23. data/lib/acfs/request/callbacks.rb +54 -0
  24. data/lib/acfs/resource.rb +39 -0
  25. data/lib/acfs/resource/attributes.rb +270 -0
  26. data/lib/acfs/resource/attributes/base.rb +29 -0
  27. data/lib/acfs/resource/attributes/boolean.rb +39 -0
  28. data/lib/acfs/resource/attributes/date_time.rb +32 -0
  29. data/lib/acfs/resource/attributes/dict.rb +39 -0
  30. data/lib/acfs/resource/attributes/float.rb +33 -0
  31. data/lib/acfs/resource/attributes/integer.rb +29 -0
  32. data/lib/acfs/resource/attributes/list.rb +36 -0
  33. data/lib/acfs/resource/attributes/string.rb +26 -0
  34. data/lib/acfs/resource/attributes/uuid.rb +48 -0
  35. data/lib/acfs/resource/dirty.rb +37 -0
  36. data/lib/acfs/resource/initialization.rb +31 -0
  37. data/lib/acfs/resource/loadable.rb +35 -0
  38. data/lib/acfs/resource/locatable.rb +135 -0
  39. data/lib/acfs/resource/operational.rb +26 -0
  40. data/lib/acfs/resource/persistence.rb +258 -0
  41. data/lib/acfs/resource/query_methods.rb +266 -0
  42. data/lib/acfs/resource/service.rb +44 -0
  43. data/lib/acfs/resource/validation.rb +49 -0
  44. data/lib/acfs/response.rb +30 -0
  45. data/lib/acfs/response/formats.rb +27 -0
  46. data/lib/acfs/response/status.rb +33 -0
  47. data/lib/acfs/rspec.rb +13 -0
  48. data/lib/acfs/runner.rb +102 -0
  49. data/lib/acfs/service.rb +94 -0
  50. data/lib/acfs/service/middleware.rb +58 -0
  51. data/lib/acfs/service/middleware/stack.rb +65 -0
  52. data/lib/acfs/singleton_resource.rb +85 -0
  53. data/lib/acfs/stub.rb +199 -0
  54. data/lib/acfs/util.rb +22 -0
  55. data/lib/acfs/version.rb +16 -0
  56. data/lib/acfs/yard.rb +6 -0
  57. data/spec/acfs/adapter/typhoeus_spec.rb +55 -0
  58. data/spec/acfs/collection_spec.rb +157 -0
  59. data/spec/acfs/configuration_spec.rb +53 -0
  60. data/spec/acfs/global_spec.rb +140 -0
  61. data/spec/acfs/location_spec.rb +25 -0
  62. data/spec/acfs/middleware/json_spec.rb +79 -0
  63. data/spec/acfs/middleware/msgpack_spec.rb +62 -0
  64. data/spec/acfs/operation_spec.rb +12 -0
  65. data/spec/acfs/request/callbacks_spec.rb +48 -0
  66. data/spec/acfs/request_spec.rb +79 -0
  67. data/spec/acfs/resource/attributes/boolean_spec.rb +58 -0
  68. data/spec/acfs/resource/attributes/date_time_spec.rb +51 -0
  69. data/spec/acfs/resource/attributes/dict_spec.rb +77 -0
  70. data/spec/acfs/resource/attributes/float_spec.rb +61 -0
  71. data/spec/acfs/resource/attributes/integer_spec.rb +36 -0
  72. data/spec/acfs/resource/attributes/list_spec.rb +60 -0
  73. data/spec/acfs/resource/attributes/uuid_spec.rb +42 -0
  74. data/spec/acfs/resource/attributes_spec.rb +179 -0
  75. data/spec/acfs/resource/dirty_spec.rb +49 -0
  76. data/spec/acfs/resource/initialization_spec.rb +36 -0
  77. data/spec/acfs/resource/loadable_spec.rb +22 -0
  78. data/spec/acfs/resource/locatable_spec.rb +118 -0
  79. data/spec/acfs/resource/persistance_spec.rb +322 -0
  80. data/spec/acfs/resource/query_methods_spec.rb +548 -0
  81. data/spec/acfs/resource/validation_spec.rb +129 -0
  82. data/spec/acfs/response/formats_spec.rb +52 -0
  83. data/spec/acfs/response/status_spec.rb +71 -0
  84. data/spec/acfs/runner_spec.rb +95 -0
  85. data/spec/acfs/service/middleware_spec.rb +35 -0
  86. data/spec/acfs/service_spec.rb +48 -0
  87. data/spec/acfs/singleton_resource_spec.rb +17 -0
  88. data/spec/acfs/stub_spec.rb +345 -0
  89. data/spec/acfs_spec.rb +205 -0
  90. data/spec/fixtures/config.yml +14 -0
  91. data/spec/spec_helper.rb +42 -0
  92. data/spec/support/hash.rb +11 -0
  93. data/spec/support/response.rb +12 -0
  94. data/spec/support/service.rb +92 -0
  95. data/spec/support/shared/find_callbacks.rb +50 -0
  96. metadata +159 -26
@@ -0,0 +1,65 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Acfs
4
+ class Service
5
+ module Middleware
6
+ class Stack
7
+ include Enumerable
8
+
9
+ MUTEX = Mutex.new
10
+ IDENTITY = ->(i) { i }
11
+
12
+ attr_reader :middlewares
13
+
14
+ def initialize
15
+ @middlewares = []
16
+ end
17
+
18
+ def call(request)
19
+ build! unless @app
20
+
21
+ @app.call request
22
+ end
23
+
24
+ def build!
25
+ return if @app
26
+
27
+ MUTEX.synchronize do
28
+ return if @app
29
+
30
+ @app = build
31
+ end
32
+ end
33
+
34
+ def build(app = IDENTITY)
35
+ middlewares.reverse.inject(app) do |next_middleware, current_middleware|
36
+ klass, args, block = current_middleware
37
+ args ||= []
38
+
39
+ if klass.is_a?(Class)
40
+ klass.new(next_middleware, *args, &block)
41
+ elsif klass.respond_to?(:call)
42
+ lambda do |env|
43
+ next_middleware.call(klass.call(env, *args))
44
+ end
45
+ else
46
+ raise "Invalid middleware, doesn't respond to `call`: #{klass.inspect}"
47
+ end
48
+ end
49
+ end
50
+
51
+ def insert(index, klass, *args, &block)
52
+ middlewares.insert(index, [klass, args, block])
53
+ end
54
+
55
+ def each
56
+ middlewares.each {|x| yield x.first }
57
+ end
58
+
59
+ def clear
60
+ middlewares.clear
61
+ end
62
+ end
63
+ end
64
+ end
65
+ end
@@ -0,0 +1,85 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Acfs
4
+ # Acfs SingletonResources
5
+ #
6
+ # Usage explanation:
7
+ # Single.find => sends GET request to http://service:port/single
8
+ # my_single.save => sends POST request to http://service:port/single
9
+ # if my_single is a new object
10
+ # or sends PUT request to http://service:port/single
11
+ # if my_single has been requested before
12
+ # my_single.delete => sends DELETE request to http://service:port/single
13
+ #
14
+ # SingletonResources do not support the Resource method :all, since
15
+ # always only a single instance of the resource is being returned
16
+ #
17
+ class SingletonResource < Acfs::Resource
18
+ # @api public
19
+ #
20
+ # Destroy resource by sending a DELETE request.
21
+ # Will raise an error in case something goes wrong.
22
+ #
23
+ # Deleting a resource is a synchronous operation.
24
+ #
25
+ # @raise [Acfs::ErroneousResponse]
26
+ # If remote service respond with not successful response.
27
+ # @return [undefined]
28
+ # @see #delete
29
+ #
30
+ def delete!(**opts)
31
+ opts[:params] ||= {}
32
+
33
+ operation(:delete, **opts) do |data|
34
+ update_with data
35
+ freeze
36
+ end
37
+ end
38
+
39
+ # @api private
40
+ def need_primary_key?
41
+ false
42
+ end
43
+
44
+ class << self
45
+ # @api public
46
+ #
47
+ # @overload find(id, opts = {})
48
+ # Find a singleton resource, optionally with params.
49
+ #
50
+ # @example
51
+ # single = Singleton.find # Will query `http://base.url/singletons/`
52
+ #
53
+ # @param [ Hash ] opts Additional options.
54
+ # @option opts [ Hash ] :params Additional parameters added to request.
55
+ #
56
+ # @yield [ resource ] Callback block to be executed after
57
+ # resource was fetched successfully.
58
+ # @yieldparam resource [ self ] Fetched resources.
59
+ #
60
+ # @return [ self ] Resource object.
61
+ #
62
+ def find(*attrs, &block)
63
+ find_single nil, params: attrs.extract_options!, &block
64
+ end
65
+
66
+ # @api public
67
+ #
68
+ # Undefined, raises NoMethodError.
69
+ # A singleton always only returns one object, therefore the
70
+ # methods :all and :where are not defined.
71
+ # :find_by is not defined on singletons, use :find instead
72
+ #
73
+ def all
74
+ raise ::Acfs::UnsupportedOperation.new
75
+ end
76
+ alias find_by all
77
+ alias find_by! all
78
+
79
+ # @api private
80
+ def location_default_path(_, path)
81
+ path
82
+ end
83
+ end
84
+ end
85
+ end
@@ -0,0 +1,199 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'rack/utils'
4
+
5
+ module Acfs
6
+ # Global handler for stubbing resources.
7
+ #
8
+ class Stub
9
+ ACTIONS = %i[read create update delete list].freeze
10
+
11
+ attr_reader :opts
12
+
13
+ def initialize(opts)
14
+ @opts = opts
15
+
16
+ @opts[:with].stringify_keys! if @opts[:with].is_a? Hash
17
+ @opts[:return].stringify_keys! if @opts[:return].is_a? Hash
18
+
19
+ if @opts[:return].is_a?(Array) # rubocop:disable Style/GuardClause
20
+ @opts[:return].map! {|h| h.stringify_keys! if h.is_a? Hash }
21
+ end
22
+ end
23
+
24
+ def accept?(op)
25
+ return opts[:with].call(op) if opts[:with].respond_to?(:call)
26
+
27
+ params = op.full_params.stringify_keys
28
+ data = op.data.stringify_keys
29
+ with = opts[:with]
30
+
31
+ return true if with.nil?
32
+
33
+ case opts.fetch(:match, :inclusion)
34
+ when :legacy
35
+ return true if with.empty? && params.empty? && data.empty?
36
+ if with.reject {|_, v| v.nil? } == params.reject {|_, v| v.nil? }
37
+ return true
38
+ end
39
+ if with.reject {|_, v| v.nil? } == data.reject {|_, v| v.nil? }
40
+ return true
41
+ end
42
+
43
+ false
44
+ when :inclusion
45
+ with.each_pair.all? do |k, v|
46
+ (params.key?(k) && params[k] == v) || (data.key?(k) && data[k] == v)
47
+ end
48
+ end
49
+ end
50
+
51
+ def calls
52
+ @calls ||= []
53
+ end
54
+
55
+ def called?(count = nil)
56
+ count = count.count if count.respond_to?(:count)
57
+
58
+ count.nil? ? calls.any? : calls.size == count
59
+ end
60
+
61
+ def call(op)
62
+ calls << op
63
+
64
+ err = opts[:raise]
65
+ data = opts[:return]
66
+
67
+ if err
68
+ raise_error op, err, opts[:return]
69
+ elsif data
70
+ data = data.call(op) if data.respond_to?(:call)
71
+
72
+ response = Acfs::Response.new op.request,
73
+ headers: opts[:headers] || {},
74
+ status: opts[:status] || 200,
75
+ data: data || {}
76
+ op.call data, response
77
+ else
78
+ raise ArgumentError.new 'Unsupported stub.'
79
+ end
80
+ end
81
+
82
+ private
83
+
84
+ def raise_error(op, name, data)
85
+ raise name if name.is_a? Class
86
+
87
+ data.stringify_keys! if data.respond_to?(:stringify_keys!)
88
+
89
+ op.handle_failure ::Acfs::Response.new(
90
+ op.request,
91
+ status: Rack::Utils.status_code(name),
92
+ data: data
93
+ )
94
+ end
95
+
96
+ class << self
97
+ # Stub a resource with given handler block. An already created handler
98
+ # for same resource class will be overridden.
99
+ #
100
+ def resource(klass, action, opts = {}, &_block)
101
+ action = action.to_sym
102
+ unless ACTIONS.include? action
103
+ raise ArgumentError.new "Unknown action `#{action}`."
104
+ end
105
+
106
+ Stub.new(opts).tap do |stub|
107
+ stubs[klass] ||= {}
108
+ stubs[klass][action] ||= []
109
+ stubs[klass][action] << stub
110
+ end
111
+ end
112
+
113
+ def allow_requests=(allow)
114
+ @allow_requests = allow ? true : false
115
+ end
116
+
117
+ def allow_requests?
118
+ @allow_requests ||= false
119
+ end
120
+
121
+ def enabled?
122
+ @enabled ||= false
123
+ end
124
+
125
+ def enable
126
+ @enabled = true
127
+ end
128
+
129
+ def disable
130
+ @enabled = false
131
+ end
132
+
133
+ # Clear all stubs.
134
+ #
135
+ def clear(klass = nil)
136
+ klass.nil? ? stubs.clear : stubs[klass].try(:clear)
137
+ end
138
+
139
+ def stubs
140
+ @stubs ||= {}
141
+ end
142
+
143
+ def stub_for(op)
144
+ return false unless (classes = stubs[op.resource])
145
+ return false unless (stubs = classes[op.action])
146
+
147
+ accepted_stubs = stubs.select {|stub| stub.accept? op }
148
+
149
+ if accepted_stubs.size > 1
150
+ raise AmbiguousStubError.new stubs: accepted_stubs, operation: op
151
+ end
152
+
153
+ accepted_stubs.first
154
+ end
155
+
156
+ def stubbed(op)
157
+ stub = stub_for op
158
+ unless stub
159
+ return false if allow_requests?
160
+
161
+ raise RealRequestsNotAllowedError.new <<~ERROR
162
+ No stub found for `#{op.action}' on `#{op.resource.name}' \
163
+ with params `#{op.full_params.inspect}', data `#{op.data.inspect}' \
164
+ and id `#{op.id}'.
165
+
166
+ Available stubs:
167
+ #{pretty_print}
168
+ ERROR
169
+ end
170
+
171
+ stub.call op
172
+ true
173
+ end
174
+
175
+ private
176
+
177
+ def pretty_print
178
+ out = ''
179
+ stubs.each do |klass, actions|
180
+ out << ' ' << klass.name << ":\n"
181
+ actions.each do |action, stubs|
182
+ stubs.each do |stub|
183
+ out << " #{action}"
184
+ out << " with #{stub.opts[:with].inspect}" if stub.opts[:with]
185
+ if stub.opts[:return]
186
+ out << " and return #{stub.opts[:return].inspect}"
187
+ end
188
+ if stub.opts[:raise]
189
+ out << " and raise #{stub.opts[:raise].inspect}"
190
+ end
191
+ out << "\n"
192
+ end
193
+ end
194
+ end
195
+ out
196
+ end
197
+ end
198
+ end
199
+ end
@@ -0,0 +1,22 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Acfs
4
+ module Util
5
+ # TODO: Merge wit features in v1.0
6
+ module Callbacks
7
+ def __callbacks__
8
+ @__callbacks__ ||= []
9
+ end
10
+
11
+ def __invoke__
12
+ __callbacks__.each {|c| c.call self }
13
+ end
14
+ end
15
+
16
+ # TODO: Replace delegator with promise or future for the long run.
17
+ class ResourceDelegator < SimpleDelegator
18
+ delegate :class, :is_a?, :kind_of?, :nil?, to: :__getobj__
19
+ include Callbacks
20
+ end
21
+ end
22
+ end
@@ -0,0 +1,16 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Acfs
4
+ module VERSION
5
+ MAJOR = 1
6
+ MINOR = 6
7
+ PATCH = 0
8
+ STAGE = nil
9
+
10
+ STRING = [MAJOR, MINOR, PATCH, STAGE].reject(&:nil?).join('.')
11
+
12
+ def self.to_s
13
+ STRING
14
+ end
15
+ end
16
+ end
@@ -0,0 +1,6 @@
1
+ # frozen_string_literal: true
2
+ # YARD macros
3
+
4
+ # @!macro [new] experimental
5
+ # @api experimental
6
+ # @note This class or method is *experimental*. It may change without further notice or major version bump.
@@ -0,0 +1,55 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'spec_helper'
4
+
5
+ describe Acfs::Adapter::Typhoeus do
6
+ let(:adapter) { described_class.new }
7
+
8
+ before do
9
+ stub_request(:any, 'http://example.org').to_return status: 200
10
+ end
11
+
12
+ it 'raises an error' do
13
+ request1 = Acfs::Request.new 'http://example.org' do |_rsp|
14
+ raise '404-1'
15
+ end
16
+ request2 = Acfs::Request.new 'http://example.org' do |_rsp|
17
+ raise '404-2'
18
+ end
19
+ adapter.queue request1
20
+ adapter.queue request2
21
+
22
+ expect { adapter.start }.to raise_error(/404-[12]/)
23
+ expect { adapter.start }.to_not raise_error
24
+ end
25
+
26
+ it 'raises timeout' do
27
+ stub_request(:any, 'http://example.org').to_timeout
28
+
29
+ request = Acfs::Request.new 'http://example.org'
30
+ adapter.queue request
31
+
32
+ expect { adapter.run(request) }.to raise_error(::Acfs::TimeoutError) do |err|
33
+ expect(err.message).to eq 'Timeout reached: GET http://example.org'
34
+ end
35
+ end
36
+
37
+ it 'raises connection errors' do
38
+ WebMock.allow_net_connect!
39
+
40
+ request = Acfs::Request.new 'http://should-never-exists.example.org'
41
+ adapter.queue request
42
+
43
+ expect { adapter.run(request) }.to raise_error(::Acfs::RequestError) do |err|
44
+ expect(err.message).to eq 'Couldn\'t resolve host name: GET http://should-never-exists.example.org'
45
+ end
46
+ end
47
+
48
+ it 'passes arguments to typhoeus hydra' do
49
+ value = {key: 1, key2: 2}
50
+
51
+ expect(::Typhoeus::Hydra).to receive(:new).with(value)
52
+
53
+ described_class.new(**value).send :hydra
54
+ end
55
+ end