acfs 1.3.3 → 1.6.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 (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