acfs 1.3.3 → 1.3.4
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/CHANGELOG.md +339 -0
- data/LICENSE +22 -0
- data/README.md +335 -0
- data/acfs.gemspec +46 -0
- data/lib/acfs.rb +51 -0
- data/lib/acfs/adapter/base.rb +24 -0
- data/lib/acfs/adapter/typhoeus.rb +69 -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 +127 -0
- data/lib/acfs/global.rb +101 -0
- data/lib/acfs/location.rb +82 -0
- data/lib/acfs/middleware/base.rb +24 -0
- data/lib/acfs/middleware/json.rb +29 -0
- data/lib/acfs/middleware/logger.rb +25 -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 +83 -0
- data/lib/acfs/request.rb +39 -0
- data/lib/acfs/request/callbacks.rb +54 -0
- data/lib/acfs/resource.rb +39 -0
- data/lib/acfs/resource/attributes.rb +269 -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 +132 -0
- data/lib/acfs/resource/operational.rb +23 -0
- data/lib/acfs/resource/persistence.rb +260 -0
- data/lib/acfs/resource/query_methods.rb +266 -0
- data/lib/acfs/resource/service.rb +44 -0
- data/lib/acfs/resource/validation.rb +39 -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 +97 -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 +194 -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 +65 -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 +181 -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 +43 -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 +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
|
data/lib/acfs/stub.rb
ADDED
@@ -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
|
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
|