acfs 1.3.3 → 1.3.4

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 +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