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
data/lib/acfs/global.rb
ADDED
@@ -0,0 +1,101 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Acfs
|
4
|
+
#
|
5
|
+
# Global Acfs module methods.
|
6
|
+
#
|
7
|
+
module Global
|
8
|
+
#
|
9
|
+
# @api private
|
10
|
+
# @return [Runner]
|
11
|
+
#
|
12
|
+
def runner
|
13
|
+
Thread.current[:acfs_runner] ||= begin
|
14
|
+
adapter = Configuration.current.adapter
|
15
|
+
|
16
|
+
if adapter
|
17
|
+
Runner.new adapter.call
|
18
|
+
else
|
19
|
+
Runner.new Adapter::Typhoeus.new
|
20
|
+
end
|
21
|
+
end
|
22
|
+
end
|
23
|
+
|
24
|
+
# @api public
|
25
|
+
#
|
26
|
+
# Run all queued operations.
|
27
|
+
#
|
28
|
+
# @return [undefined]
|
29
|
+
#
|
30
|
+
def run
|
31
|
+
::ActiveSupport::Notifications.instrument 'acfs.before_run'
|
32
|
+
::ActiveSupport::Notifications.instrument 'acfs.run' do
|
33
|
+
runner.start
|
34
|
+
end
|
35
|
+
end
|
36
|
+
|
37
|
+
# @api public
|
38
|
+
#
|
39
|
+
# Configure acfs using given block.
|
40
|
+
#
|
41
|
+
# @return [undefined]
|
42
|
+
# @see Configuration#configure
|
43
|
+
#
|
44
|
+
def configure(&block)
|
45
|
+
Configuration.current.configure(&block)
|
46
|
+
end
|
47
|
+
|
48
|
+
# @api public
|
49
|
+
#
|
50
|
+
# Reset all queues, stubs and internal state.
|
51
|
+
#
|
52
|
+
def reset
|
53
|
+
::ActiveSupport::Notifications.instrument 'acfs.reset' do
|
54
|
+
runner.clear
|
55
|
+
Acfs::Stub.clear
|
56
|
+
end
|
57
|
+
end
|
58
|
+
|
59
|
+
# @api public
|
60
|
+
#
|
61
|
+
# Add an additional callback hook to not loaded resource.
|
62
|
+
# If given resource already loaded callback will be invoked immediately.
|
63
|
+
#
|
64
|
+
# This method will be replaced by explicit callback
|
65
|
+
# handling when query methods return explicit future objects.
|
66
|
+
#
|
67
|
+
# @example
|
68
|
+
# user = MyUser.find 1, &callback_one
|
69
|
+
# Acfs.add_callback(user, &callback_two)
|
70
|
+
#
|
71
|
+
def add_callback(resource, &block)
|
72
|
+
unless resource.respond_to?(:__callbacks__)
|
73
|
+
raise ArgumentError.new 'Given resource is not an Acfs resource ' \
|
74
|
+
"delegator but a: #{resource.class.name}"
|
75
|
+
end
|
76
|
+
return false if block.nil?
|
77
|
+
|
78
|
+
if resource.nil? || resource.loaded?
|
79
|
+
block.call resource
|
80
|
+
else
|
81
|
+
resource.__callbacks__ << block
|
82
|
+
end
|
83
|
+
end
|
84
|
+
|
85
|
+
def on(*resources)
|
86
|
+
# If all resources have already been loaded, we run the callback immediately.
|
87
|
+
if resources.all? {|res| res.nil? || res.loaded? }
|
88
|
+
yield(*resources)
|
89
|
+
return
|
90
|
+
end
|
91
|
+
|
92
|
+
# Otherwise, we add a callback to *each* resource with a guard that ensures
|
93
|
+
# that only the very last resource being loaded executes the callback.
|
94
|
+
resources.each do |resource|
|
95
|
+
add_callback resource do |_|
|
96
|
+
yield(*resources) if resources.all? {|res| res.nil? || res.loaded? }
|
97
|
+
end
|
98
|
+
end
|
99
|
+
end
|
100
|
+
end
|
101
|
+
end
|
@@ -0,0 +1,76 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Acfs
|
4
|
+
# @api private
|
5
|
+
#
|
6
|
+
# Describes a URL with placeholders.
|
7
|
+
#
|
8
|
+
class Location
|
9
|
+
attr_reader :arguments, :raw, :struct, :vars
|
10
|
+
|
11
|
+
REGEXP = /^:([A-z][A-z0-9_]*)$/.freeze
|
12
|
+
|
13
|
+
def initialize(uri, vars = {})
|
14
|
+
@raw = URI.parse uri
|
15
|
+
@vars = vars
|
16
|
+
@struct = raw.path.split('/').reject(&:empty?).map {|s| s =~ REGEXP ? Regexp.last_match[1].to_sym : s }
|
17
|
+
@arguments = struct.select {|s| s.is_a?(Symbol) }
|
18
|
+
end
|
19
|
+
|
20
|
+
def build(vars)
|
21
|
+
self.class.new raw.to_s, vars.stringify_keys.merge(self.vars)
|
22
|
+
end
|
23
|
+
|
24
|
+
def extract_from(*args)
|
25
|
+
vars = {}
|
26
|
+
arguments.each {|key| vars[key.to_s] = extract_arg(key, args) }
|
27
|
+
|
28
|
+
build(vars)
|
29
|
+
end
|
30
|
+
|
31
|
+
def str
|
32
|
+
uri = raw.dup
|
33
|
+
uri.path = "/#{struct.map(&method(:lookup_variable)).join('/')}"
|
34
|
+
uri.to_s
|
35
|
+
end
|
36
|
+
|
37
|
+
def raw_uri
|
38
|
+
raw.to_s
|
39
|
+
end
|
40
|
+
alias to_s raw_uri
|
41
|
+
|
42
|
+
private
|
43
|
+
|
44
|
+
def extract_arg(key, hashes)
|
45
|
+
hashes.each_with_index do |hash, index|
|
46
|
+
if hash.key?(key)
|
47
|
+
return (index.zero? ? hash.delete(key) : hash.fetch(key))
|
48
|
+
end
|
49
|
+
end
|
50
|
+
|
51
|
+
nil
|
52
|
+
end
|
53
|
+
|
54
|
+
def lookup_variable(name)
|
55
|
+
return name unless name.is_a?(Symbol)
|
56
|
+
|
57
|
+
value = vars.fetch(name.to_s) do
|
58
|
+
if @raise.nil? || @raise
|
59
|
+
raise ArgumentError.new <<~ERROR.strip
|
60
|
+
URI path argument `#{name}' missing on `#{self}'. Given: `#{vars}.inspect'
|
61
|
+
ERROR
|
62
|
+
end
|
63
|
+
|
64
|
+
":#{name}"
|
65
|
+
end
|
66
|
+
|
67
|
+
value = value.to_s.strip
|
68
|
+
|
69
|
+
if value.empty?
|
70
|
+
raise ArgumentError.new "Cannot replace path argument `#{name}' with empty string."
|
71
|
+
end
|
72
|
+
|
73
|
+
::URI.encode_www_form_component(value)
|
74
|
+
end
|
75
|
+
end
|
76
|
+
end
|
@@ -0,0 +1,24 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Acfs
|
4
|
+
module Middleware
|
5
|
+
# A base middleware that does not modify request or response.
|
6
|
+
# Can be used as super class for custom middleware implementations.
|
7
|
+
#
|
8
|
+
class Base
|
9
|
+
attr_reader :app, :options
|
10
|
+
|
11
|
+
def initialize(app, **opts)
|
12
|
+
@app = app
|
13
|
+
@options = opts
|
14
|
+
end
|
15
|
+
|
16
|
+
def call(request)
|
17
|
+
if respond_to? :response
|
18
|
+
request.on_complete {|res, nxt| response(res, nxt) }
|
19
|
+
end
|
20
|
+
app.call(request)
|
21
|
+
end
|
22
|
+
end
|
23
|
+
end
|
24
|
+
end
|
@@ -0,0 +1,31 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'multi_json'
|
4
|
+
|
5
|
+
module Acfs
|
6
|
+
module Middleware
|
7
|
+
# A middleware to encore request data using JSON.
|
8
|
+
#
|
9
|
+
class JSON < Serializer
|
10
|
+
def mime
|
11
|
+
::Mime[:json]
|
12
|
+
end
|
13
|
+
|
14
|
+
def encode(data)
|
15
|
+
::MultiJson.dump(data)
|
16
|
+
end
|
17
|
+
|
18
|
+
def decode(body)
|
19
|
+
::MultiJson.load(body)
|
20
|
+
rescue ::MultiJson::ParseError => e
|
21
|
+
raise ::JSON::ParserError.new(e)
|
22
|
+
end
|
23
|
+
end
|
24
|
+
|
25
|
+
# @deprecated
|
26
|
+
JsonDecoder = JSON
|
27
|
+
|
28
|
+
# @deprecated
|
29
|
+
JsonEncoder = JSON
|
30
|
+
end
|
31
|
+
end
|
@@ -0,0 +1,23 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'logger'
|
4
|
+
|
5
|
+
module Acfs
|
6
|
+
module Middleware
|
7
|
+
# Log requests and responses.
|
8
|
+
#
|
9
|
+
class Logger < Base
|
10
|
+
attr_reader :logger
|
11
|
+
|
12
|
+
def initialize(app, **opts)
|
13
|
+
super
|
14
|
+
@logger = options[:logger] || ::Logger.new($stdout)
|
15
|
+
end
|
16
|
+
|
17
|
+
def response(res, nxt)
|
18
|
+
logger.info "[ACFS] #{res.request.method.to_s.upcase} #{res.request.url} -> #{res.status}"
|
19
|
+
nxt.call res
|
20
|
+
end
|
21
|
+
end
|
22
|
+
end
|
23
|
+
end
|
@@ -0,0 +1,32 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'msgpack'
|
4
|
+
require 'action_dispatch'
|
5
|
+
|
6
|
+
module Acfs
|
7
|
+
module Middleware
|
8
|
+
class MessagePack < Serializer
|
9
|
+
unless defined?(::Mime::MSGPACK)
|
10
|
+
::Mime::Type.register 'application/x-msgpack', :msgpack
|
11
|
+
end
|
12
|
+
|
13
|
+
def mime
|
14
|
+
::Mime[:msgpack]
|
15
|
+
end
|
16
|
+
|
17
|
+
def encode(data)
|
18
|
+
::MessagePack.pack data
|
19
|
+
end
|
20
|
+
|
21
|
+
def decode(body)
|
22
|
+
::MessagePack.unpack body
|
23
|
+
end
|
24
|
+
end
|
25
|
+
|
26
|
+
# @deprecated
|
27
|
+
MessagePackEncoder = MessagePack
|
28
|
+
|
29
|
+
# @deprecated
|
30
|
+
MessagePackDecoder = MessagePack
|
31
|
+
end
|
32
|
+
end
|
@@ -0,0 +1,23 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Acfs
|
4
|
+
module Middleware
|
5
|
+
# Print resquests and response on terminal
|
6
|
+
#
|
7
|
+
class Print < Base
|
8
|
+
def call(req)
|
9
|
+
puts '-' * 80
|
10
|
+
puts req.inspect
|
11
|
+
puts '-' * 80
|
12
|
+
|
13
|
+
super
|
14
|
+
end
|
15
|
+
|
16
|
+
def response(res)
|
17
|
+
puts '-' * 80
|
18
|
+
puts res.inspect
|
19
|
+
puts '-' * 80
|
20
|
+
end
|
21
|
+
end
|
22
|
+
end
|
23
|
+
end
|
@@ -0,0 +1,41 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Acfs
|
4
|
+
module Middleware
|
5
|
+
# A base middleware that does not modify request or response.
|
6
|
+
# Can be used as super class for custom middleware implementations.
|
7
|
+
#
|
8
|
+
class Serializer < Base
|
9
|
+
def encode(_data)
|
10
|
+
raise NotImplementedError
|
11
|
+
end
|
12
|
+
|
13
|
+
def decode(_data)
|
14
|
+
raise NotImplementedError
|
15
|
+
end
|
16
|
+
|
17
|
+
def mime
|
18
|
+
raise NotImplementedError
|
19
|
+
end
|
20
|
+
|
21
|
+
def call(request)
|
22
|
+
unless request.headers['Content-Type']
|
23
|
+
request.body = encode request.data
|
24
|
+
request.headers['Content-Type'] = mime
|
25
|
+
end
|
26
|
+
|
27
|
+
accept = request.headers['Accept'].to_s.split(',')
|
28
|
+
accept << "#{mime};q=#{options.fetch(:q, 1)}"
|
29
|
+
request.headers['Accept'] = accept.join(',')
|
30
|
+
|
31
|
+
request.on_complete do |response, nxt|
|
32
|
+
response.data = decode(response.body) if mime == response.content_type
|
33
|
+
|
34
|
+
nxt.call response
|
35
|
+
end
|
36
|
+
|
37
|
+
app.call(request)
|
38
|
+
end
|
39
|
+
end
|
40
|
+
end
|
41
|
+
end
|
@@ -0,0 +1,96 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Acfs
|
4
|
+
# @api private
|
5
|
+
#
|
6
|
+
# Describes a CRUD operation. Handle request creation and response
|
7
|
+
# processing as well as error handling and stubbing.
|
8
|
+
#
|
9
|
+
class Operation
|
10
|
+
attr_reader :action, :params, :resource, :data, :callback, :location, :url
|
11
|
+
|
12
|
+
delegate :service, to: :resource
|
13
|
+
delegate :call, to: :callback
|
14
|
+
|
15
|
+
def initialize(resource, action, **opts, &block)
|
16
|
+
@resource = resource
|
17
|
+
@action = action.to_sym
|
18
|
+
|
19
|
+
# Operations can be delayed so dup params and data to avoid
|
20
|
+
# later in-place changes by modifying passed hash
|
21
|
+
@params = (opts[:params] || {}).dup
|
22
|
+
@data = (opts[:data] || {}).dup
|
23
|
+
|
24
|
+
unless (@url = opts[:url])
|
25
|
+
@location = resource.location(action: @action).extract_from(@params, @data)
|
26
|
+
@url = location.str
|
27
|
+
end
|
28
|
+
|
29
|
+
@callback = block
|
30
|
+
end
|
31
|
+
|
32
|
+
def single?
|
33
|
+
%i[read update delete].include? action
|
34
|
+
end
|
35
|
+
|
36
|
+
def synchronous?
|
37
|
+
%i[update delete create].include? action
|
38
|
+
end
|
39
|
+
|
40
|
+
def id
|
41
|
+
# TODO
|
42
|
+
@id ||= params.delete(:id) || data[:id]
|
43
|
+
end
|
44
|
+
|
45
|
+
def full_params
|
46
|
+
(id ? params.merge(id: id) : params).merge(location_vars)
|
47
|
+
end
|
48
|
+
|
49
|
+
def location_vars
|
50
|
+
location ? location.vars : {}
|
51
|
+
end
|
52
|
+
|
53
|
+
def method
|
54
|
+
{read: :get, list: :get, update: :put, create: :post, delete: :delete}[action]
|
55
|
+
end
|
56
|
+
|
57
|
+
def request
|
58
|
+
request = ::Acfs::Request.new url, method: method, params: params,
|
59
|
+
data: data, operation: self
|
60
|
+
request.on_complete do |response|
|
61
|
+
::ActiveSupport::Notifications.instrument 'acfs.operation.complete',
|
62
|
+
operation: self,
|
63
|
+
response: response
|
64
|
+
|
65
|
+
handle_failure response unless response.success?
|
66
|
+
callback.call response.data, response
|
67
|
+
end
|
68
|
+
request
|
69
|
+
end
|
70
|
+
|
71
|
+
def handle_failure(response)
|
72
|
+
case response.code
|
73
|
+
when 400
|
74
|
+
raise ::Acfs::BadRequest.new response: response
|
75
|
+
when 401
|
76
|
+
raise ::Acfs::Unauthorized.new response: response
|
77
|
+
when 403
|
78
|
+
raise ::Acfs::Forbidden.new response: response
|
79
|
+
when 404
|
80
|
+
raise ::Acfs::ResourceNotFound.new response: response
|
81
|
+
when 422
|
82
|
+
raise ::Acfs::InvalidResource.new response: response, errors: response.data.try(:[], 'errors')
|
83
|
+
when 500
|
84
|
+
raise ::Acfs::ServerError.new response: response
|
85
|
+
when 502
|
86
|
+
raise ::Acfs::BadGateway.new response: response
|
87
|
+
when 503
|
88
|
+
raise ::Acfs::ServiceUnavailable.new response: response
|
89
|
+
when 504
|
90
|
+
raise ::Acfs::GatewayTimeout.new response: response
|
91
|
+
else
|
92
|
+
raise ::Acfs::ErroneousResponse.new response: response
|
93
|
+
end
|
94
|
+
end
|
95
|
+
end
|
96
|
+
end
|