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.
- checksums.yaml +4 -4
- data/CHANGELOG.md +372 -0
- data/LICENSE +22 -0
- data/README.md +321 -0
- data/acfs.gemspec +38 -0
- data/lib/acfs.rb +51 -0
- data/lib/acfs/adapter/base.rb +26 -0
- data/lib/acfs/adapter/typhoeus.rb +82 -0
- data/lib/acfs/collection.rb +28 -0
- data/lib/acfs/collections/paginatable.rb +76 -0
- data/lib/acfs/configuration.rb +120 -0
- data/lib/acfs/errors.rb +147 -0
- data/lib/acfs/global.rb +101 -0
- data/lib/acfs/location.rb +76 -0
- data/lib/acfs/middleware/base.rb +24 -0
- data/lib/acfs/middleware/json.rb +31 -0
- data/lib/acfs/middleware/logger.rb +23 -0
- data/lib/acfs/middleware/msgpack.rb +32 -0
- data/lib/acfs/middleware/print.rb +23 -0
- data/lib/acfs/middleware/serializer.rb +41 -0
- data/lib/acfs/operation.rb +96 -0
- data/lib/acfs/request.rb +32 -0
- data/lib/acfs/request/callbacks.rb +54 -0
- data/lib/acfs/resource.rb +39 -0
- data/lib/acfs/resource/attributes.rb +270 -0
- data/lib/acfs/resource/attributes/base.rb +29 -0
- data/lib/acfs/resource/attributes/boolean.rb +39 -0
- data/lib/acfs/resource/attributes/date_time.rb +32 -0
- data/lib/acfs/resource/attributes/dict.rb +39 -0
- data/lib/acfs/resource/attributes/float.rb +33 -0
- data/lib/acfs/resource/attributes/integer.rb +29 -0
- data/lib/acfs/resource/attributes/list.rb +36 -0
- data/lib/acfs/resource/attributes/string.rb +26 -0
- data/lib/acfs/resource/attributes/uuid.rb +48 -0
- data/lib/acfs/resource/dirty.rb +37 -0
- data/lib/acfs/resource/initialization.rb +31 -0
- data/lib/acfs/resource/loadable.rb +35 -0
- data/lib/acfs/resource/locatable.rb +135 -0
- data/lib/acfs/resource/operational.rb +26 -0
- data/lib/acfs/resource/persistence.rb +258 -0
- data/lib/acfs/resource/query_methods.rb +266 -0
- data/lib/acfs/resource/service.rb +44 -0
- data/lib/acfs/resource/validation.rb +49 -0
- data/lib/acfs/response.rb +30 -0
- data/lib/acfs/response/formats.rb +27 -0
- data/lib/acfs/response/status.rb +33 -0
- data/lib/acfs/rspec.rb +13 -0
- data/lib/acfs/runner.rb +102 -0
- data/lib/acfs/service.rb +94 -0
- data/lib/acfs/service/middleware.rb +58 -0
- data/lib/acfs/service/middleware/stack.rb +65 -0
- data/lib/acfs/singleton_resource.rb +85 -0
- data/lib/acfs/stub.rb +199 -0
- data/lib/acfs/util.rb +22 -0
- data/lib/acfs/version.rb +16 -0
- data/lib/acfs/yard.rb +6 -0
- data/spec/acfs/adapter/typhoeus_spec.rb +55 -0
- data/spec/acfs/collection_spec.rb +157 -0
- data/spec/acfs/configuration_spec.rb +53 -0
- data/spec/acfs/global_spec.rb +140 -0
- data/spec/acfs/location_spec.rb +25 -0
- data/spec/acfs/middleware/json_spec.rb +79 -0
- data/spec/acfs/middleware/msgpack_spec.rb +62 -0
- data/spec/acfs/operation_spec.rb +12 -0
- data/spec/acfs/request/callbacks_spec.rb +48 -0
- data/spec/acfs/request_spec.rb +79 -0
- data/spec/acfs/resource/attributes/boolean_spec.rb +58 -0
- data/spec/acfs/resource/attributes/date_time_spec.rb +51 -0
- data/spec/acfs/resource/attributes/dict_spec.rb +77 -0
- data/spec/acfs/resource/attributes/float_spec.rb +61 -0
- data/spec/acfs/resource/attributes/integer_spec.rb +36 -0
- data/spec/acfs/resource/attributes/list_spec.rb +60 -0
- data/spec/acfs/resource/attributes/uuid_spec.rb +42 -0
- data/spec/acfs/resource/attributes_spec.rb +179 -0
- data/spec/acfs/resource/dirty_spec.rb +49 -0
- data/spec/acfs/resource/initialization_spec.rb +36 -0
- data/spec/acfs/resource/loadable_spec.rb +22 -0
- data/spec/acfs/resource/locatable_spec.rb +118 -0
- data/spec/acfs/resource/persistance_spec.rb +322 -0
- data/spec/acfs/resource/query_methods_spec.rb +548 -0
- data/spec/acfs/resource/validation_spec.rb +129 -0
- data/spec/acfs/response/formats_spec.rb +52 -0
- data/spec/acfs/response/status_spec.rb +71 -0
- data/spec/acfs/runner_spec.rb +95 -0
- data/spec/acfs/service/middleware_spec.rb +35 -0
- data/spec/acfs/service_spec.rb +48 -0
- data/spec/acfs/singleton_resource_spec.rb +17 -0
- data/spec/acfs/stub_spec.rb +345 -0
- data/spec/acfs_spec.rb +205 -0
- data/spec/fixtures/config.yml +14 -0
- data/spec/spec_helper.rb +42 -0
- data/spec/support/hash.rb +11 -0
- data/spec/support/response.rb +12 -0
- data/spec/support/service.rb +92 -0
- data/spec/support/shared/find_callbacks.rb +50 -0
- 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
|
data/lib/acfs/stub.rb
ADDED
@@ -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
|
data/lib/acfs/util.rb
ADDED
@@ -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
|
data/lib/acfs/version.rb
ADDED
data/lib/acfs/yard.rb
ADDED
@@ -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
|