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.
- 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
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,82 @@
|
|
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, :args
|
10
|
+
|
11
|
+
REGEXP = /^:([A-z][A-z0-9_]*)$/.freeze
|
12
|
+
|
13
|
+
def initialize(uri, args = {})
|
14
|
+
@raw = URI.parse uri
|
15
|
+
@args = args
|
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(args = {})
|
21
|
+
unless args.is_a?(Hash)
|
22
|
+
raise ArgumentError.new "URI path arguments must be a hash, `#{args.inspect}' given."
|
23
|
+
end
|
24
|
+
|
25
|
+
self.class.new raw.to_s, args.merge(self.args)
|
26
|
+
end
|
27
|
+
|
28
|
+
def extract_from(*args)
|
29
|
+
args = {}.tap do |collect|
|
30
|
+
arguments.each {|key| collect[key] = extract_arg key, args }
|
31
|
+
end
|
32
|
+
|
33
|
+
build args
|
34
|
+
end
|
35
|
+
|
36
|
+
def str
|
37
|
+
uri = raw.dup
|
38
|
+
uri.path = '/' + struct.map {|s| lookup_arg(s, args) }.join('/')
|
39
|
+
uri.to_s
|
40
|
+
end
|
41
|
+
|
42
|
+
def raw_uri
|
43
|
+
raw.to_s
|
44
|
+
end
|
45
|
+
alias to_s raw_uri
|
46
|
+
|
47
|
+
private
|
48
|
+
|
49
|
+
def extract_arg(key, hashes)
|
50
|
+
hashes.each_with_index do |hash, index|
|
51
|
+
if hash.key?(key)
|
52
|
+
return (index == 0 ? hash.delete(key) : hash.fetch(key))
|
53
|
+
end
|
54
|
+
end
|
55
|
+
|
56
|
+
nil
|
57
|
+
end
|
58
|
+
|
59
|
+
def lookup_arg(arg, args)
|
60
|
+
arg.is_a?(Symbol) ? lookup_replacement(arg, args) : arg
|
61
|
+
end
|
62
|
+
|
63
|
+
def lookup_replacement(sym, args)
|
64
|
+
value = get_replacement(sym, args).to_s
|
65
|
+
return ::URI.encode_www_form_component(value) unless value.empty?
|
66
|
+
|
67
|
+
raise ArgumentError.new "Cannot replace path argument `#{sym}' with empty string."
|
68
|
+
end
|
69
|
+
|
70
|
+
def get_replacement(sym, args)
|
71
|
+
args.fetch(sym.to_s) do
|
72
|
+
args.fetch(sym) do
|
73
|
+
if args[:raise].nil? || args[:raise]
|
74
|
+
raise ArgumentError.new "URI path argument `#{sym}' missing on `#{self}'. Given: `#{args}.inspect'"
|
75
|
+
else
|
76
|
+
":#{sym}"
|
77
|
+
end
|
78
|
+
end
|
79
|
+
end
|
80
|
+
end
|
81
|
+
end
|
82
|
+
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, options = {})
|
12
|
+
@app = app
|
13
|
+
@options = options
|
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,29 @@
|
|
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
|
+
end
|
21
|
+
end
|
22
|
+
|
23
|
+
# @deprecated
|
24
|
+
JsonDecoder = JSON
|
25
|
+
|
26
|
+
# @deprecated
|
27
|
+
JsonEncoder = JSON
|
28
|
+
end
|
29
|
+
end
|
@@ -0,0 +1,25 @@
|
|
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
|
+
def initialize(app, options = {})
|
11
|
+
super
|
12
|
+
@logger = options[:logger] if options[:logger]
|
13
|
+
end
|
14
|
+
|
15
|
+
def response(res, nxt)
|
16
|
+
logger.info "[ACFS] #{res.request.method.to_s.upcase} #{res.request.url} -> #{res.status}"
|
17
|
+
nxt.call res
|
18
|
+
end
|
19
|
+
|
20
|
+
def logger
|
21
|
+
@logger ||= ::Logger.new STDOUT
|
22
|
+
end
|
23
|
+
end
|
24
|
+
end
|
25
|
+
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,83 @@
|
|
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
|
+
delegate :service, to: :resource
|
12
|
+
delegate :call, to: :callback
|
13
|
+
|
14
|
+
def initialize(resource, action, opts = {}, &block)
|
15
|
+
@resource = resource
|
16
|
+
@action = action.to_sym
|
17
|
+
|
18
|
+
# Operations can be delayed so dup params and data to avoid
|
19
|
+
# later in-place changes by modifying passed hash
|
20
|
+
@params = (opts[:params] || {}).dup
|
21
|
+
@data = (opts[:data] || {}).dup
|
22
|
+
|
23
|
+
if opts[:url]
|
24
|
+
@url = opts[:url]
|
25
|
+
else
|
26
|
+
@location = resource.location(action: @action).extract_from(@params, @data)
|
27
|
+
@url = location.str
|
28
|
+
end
|
29
|
+
|
30
|
+
@callback = block
|
31
|
+
end
|
32
|
+
|
33
|
+
def single?
|
34
|
+
%i[read update delete].include? action
|
35
|
+
end
|
36
|
+
|
37
|
+
def synchronous?
|
38
|
+
%i[update delete create].include? action
|
39
|
+
end
|
40
|
+
|
41
|
+
def id
|
42
|
+
# TODO
|
43
|
+
@id ||= params.delete(:id) || data[:id]
|
44
|
+
end
|
45
|
+
|
46
|
+
def full_params
|
47
|
+
(id ? params.merge(id: id) : params).merge location_args
|
48
|
+
end
|
49
|
+
|
50
|
+
def location_args
|
51
|
+
location ? location.args : {}
|
52
|
+
end
|
53
|
+
|
54
|
+
def method
|
55
|
+
{read: :get, list: :get, update: :put, create: :post, delete: :delete}[action]
|
56
|
+
end
|
57
|
+
|
58
|
+
def request
|
59
|
+
request = ::Acfs::Request.new url, method: method, params: params,
|
60
|
+
data: data, operation: self
|
61
|
+
request.on_complete do |response|
|
62
|
+
::ActiveSupport::Notifications.instrument 'acfs.operation.complete',
|
63
|
+
operation: self,
|
64
|
+
response: response
|
65
|
+
|
66
|
+
handle_failure response unless response.success?
|
67
|
+
callback.call response.data, response
|
68
|
+
end
|
69
|
+
request
|
70
|
+
end
|
71
|
+
|
72
|
+
def handle_failure(response)
|
73
|
+
case response.code
|
74
|
+
when 404
|
75
|
+
raise ::Acfs::ResourceNotFound.new response: response
|
76
|
+
when 422
|
77
|
+
raise ::Acfs::InvalidResource.new response: response, errors: response.data.try(:[], 'errors')
|
78
|
+
else
|
79
|
+
raise ::Acfs::ErroneousResponse.new response: response
|
80
|
+
end
|
81
|
+
end
|
82
|
+
end
|
83
|
+
end
|