acfs 1.3.3 → 1.3.4

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 +339 -0
  3. data/LICENSE +22 -0
  4. data/README.md +335 -0
  5. data/acfs.gemspec +46 -0
  6. data/lib/acfs.rb +51 -0
  7. data/lib/acfs/adapter/base.rb +24 -0
  8. data/lib/acfs/adapter/typhoeus.rb +69 -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 +127 -0
  13. data/lib/acfs/global.rb +101 -0
  14. data/lib/acfs/location.rb +82 -0
  15. data/lib/acfs/middleware/base.rb +24 -0
  16. data/lib/acfs/middleware/json.rb +29 -0
  17. data/lib/acfs/middleware/logger.rb +25 -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 +83 -0
  22. data/lib/acfs/request.rb +39 -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 +269 -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 +132 -0
  39. data/lib/acfs/resource/operational.rb +23 -0
  40. data/lib/acfs/resource/persistence.rb +260 -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 +39 -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 +97 -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 +194 -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 +65 -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 +181 -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 +43 -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 +136 -3
@@ -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,194 @@
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
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
+ if count.respond_to? :count
57
+ count = count.count
58
+ end # For `5.times` Enumerators
59
+ count.nil? ? calls.any? : calls.size == count
60
+ end
61
+
62
+ def call(op)
63
+ calls << op
64
+
65
+ err = opts[:raise]
66
+ data = opts[:return]
67
+
68
+ if err
69
+ raise_error op, err, opts[:return]
70
+ elsif data
71
+ data = data.call(op) if data.respond_to?(:call)
72
+
73
+ response = Acfs::Response.new op.request,
74
+ headers: opts[:headers] || {},
75
+ status: opts[:status] || 200,
76
+ data: data || {}
77
+ op.call data, response
78
+ else
79
+ raise ArgumentError.new 'Unsupported stub.'
80
+ end
81
+ end
82
+
83
+ private
84
+
85
+ def raise_error(op, name, data)
86
+ raise name if name.is_a? Class
87
+
88
+ data.stringify_keys! if data.respond_to? :stringify_keys!
89
+
90
+ op.handle_failure ::Acfs::Response.new op.request, status: Rack::Utils.status_code(name), data: data
91
+ end
92
+
93
+ class << self
94
+ # Stub a resource with given handler block. An already created handler
95
+ # for same resource class will be overridden.
96
+ #
97
+ def resource(klass, action, opts = {}, &_block)
98
+ action = action.to_sym
99
+ unless ACTIONS.include? action
100
+ raise ArgumentError.new "Unknown action `#{action}`."
101
+ end
102
+
103
+ Stub.new(opts).tap do |stub|
104
+ stubs[klass] ||= {}
105
+ stubs[klass][action] ||= []
106
+ stubs[klass][action] << stub
107
+ end
108
+ end
109
+
110
+ def allow_requests=(allow)
111
+ @allow_requests = allow ? true : false
112
+ end
113
+
114
+ def allow_requests?
115
+ @allow_requests ||= false
116
+ end
117
+
118
+ def enabled?
119
+ @enabled ||= false
120
+ end
121
+
122
+ def enable
123
+ @enabled = true
124
+ end
125
+
126
+ def disable
127
+ @enabled = false
128
+ end
129
+
130
+ # Clear all stubs.
131
+ #
132
+ def clear(klass = nil)
133
+ klass.nil? ? stubs.clear : stubs[klass].try(:clear)
134
+ end
135
+
136
+ def stubs
137
+ @stubs ||= {}
138
+ end
139
+
140
+ def stub_for(op)
141
+ return false unless (classes = stubs[op.resource])
142
+ return false unless (stubs = classes[op.action])
143
+
144
+ accepted_stubs = stubs.select {|stub| stub.accept? op }
145
+
146
+ if accepted_stubs.size > 1
147
+ raise AmbiguousStubError.new stubs: accepted_stubs, operation: op
148
+ end
149
+
150
+ accepted_stubs.first
151
+ end
152
+
153
+ def stubbed(op)
154
+ stub = stub_for op
155
+ unless stub
156
+ return false if allow_requests?
157
+
158
+ raise RealRequestsNotAllowedError.new <<-MSG.strip.gsub(/^[ ]{12}/, '')
159
+ No stub found for `#{op.action}' on `#{op.resource.name}' with params `#{op.full_params.inspect}', data `#{op.data.inspect}' and id `#{op.id}'.
160
+
161
+ Available stubs:
162
+ #{pretty_print}
163
+ MSG
164
+ end
165
+
166
+ stub.call op
167
+ true
168
+ end
169
+
170
+ private
171
+
172
+ def pretty_print
173
+ out = ''
174
+ stubs.each do |klass, actions|
175
+ out << ' ' << klass.name << ":\n"
176
+ actions.each do |action, stubs|
177
+ stubs.each do |stub|
178
+ out << " #{action}"
179
+ out << " with #{stub.opts[:with].inspect}" if stub.opts[:with]
180
+ if stub.opts[:return]
181
+ out << " and return #{stub.opts[:return].inspect}"
182
+ end
183
+ if stub.opts[:raise]
184
+ out << " and raise #{stub.opts[:raise].inspect}"
185
+ end
186
+ out << "\n"
187
+ end
188
+ end
189
+ end
190
+ out
191
+ end
192
+ end
193
+ end
194
+ 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 = 3
7
+ PATCH = 4
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