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
@@ -0,0 +1,44 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
class Acfs::Resource
|
4
|
+
# Included by Acfs::Model. Allows to configure the service
|
5
|
+
# a resource belongs to.
|
6
|
+
#
|
7
|
+
module Service
|
8
|
+
extend ActiveSupport::Concern
|
9
|
+
|
10
|
+
module ClassMethods
|
11
|
+
# @api public
|
12
|
+
#
|
13
|
+
# @overload service()
|
14
|
+
# Return service instance.
|
15
|
+
#
|
16
|
+
# @return [Service] Service class instance.
|
17
|
+
#
|
18
|
+
# @overload service(klass, options = {})
|
19
|
+
# Link to service this model belongs to. Connection
|
20
|
+
# settings like base URL are fetched from service.
|
21
|
+
# Return assigned service if no arguments are given.
|
22
|
+
#
|
23
|
+
# @example
|
24
|
+
# class AccountService < Acfs::Client
|
25
|
+
# self.base_url = 'http://acc.serv.org'
|
26
|
+
# end
|
27
|
+
#
|
28
|
+
# class MyUser < Acfs::Resource
|
29
|
+
# service AccountService
|
30
|
+
# end
|
31
|
+
# MyUser.find 5 # Will fetch `http://acc.serv.org/users/5`
|
32
|
+
#
|
33
|
+
# @param klass [Class] Service class derived from {Acfs::Service}.
|
34
|
+
# @param options [Object] Option delegated to
|
35
|
+
# service class initializer.
|
36
|
+
#
|
37
|
+
def service(klass = nil, options = {})
|
38
|
+
return (@service = klass.new options) if klass
|
39
|
+
|
40
|
+
@service || superclass.service
|
41
|
+
end
|
42
|
+
end
|
43
|
+
end
|
44
|
+
end
|
@@ -0,0 +1,39 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
class Acfs::Resource
|
4
|
+
module Validation
|
5
|
+
def valid?(*args)
|
6
|
+
super
|
7
|
+
remote_errors.each {|f, e| errors.add f, e }
|
8
|
+
errors.empty?
|
9
|
+
end
|
10
|
+
|
11
|
+
def remote_errors
|
12
|
+
@remote_errors ||= ActiveModel::Errors.new self
|
13
|
+
end
|
14
|
+
|
15
|
+
def remote_errors=(errors)
|
16
|
+
if errors.respond_to?(:each_pair)
|
17
|
+
errors.each_pair do |field, errs|
|
18
|
+
Array(errs).each do |err|
|
19
|
+
self.errors.add field.to_sym, err
|
20
|
+
remote_errors.add field.to_sym, err
|
21
|
+
end
|
22
|
+
end
|
23
|
+
else
|
24
|
+
Array(errors).each do |err|
|
25
|
+
self.errors.add :base, err
|
26
|
+
remote_errors.add :base, err
|
27
|
+
end
|
28
|
+
end
|
29
|
+
end
|
30
|
+
|
31
|
+
def save!(*_)
|
32
|
+
unless valid?(new? ? :create : :save)
|
33
|
+
raise ::Acfs::InvalidResource.new resource: self, errors: errors.to_a
|
34
|
+
end
|
35
|
+
|
36
|
+
super
|
37
|
+
end
|
38
|
+
end
|
39
|
+
end
|
@@ -0,0 +1,30 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'acfs/response/formats'
|
4
|
+
require 'acfs/response/status'
|
5
|
+
require 'active_support/core_ext/module/delegation'
|
6
|
+
|
7
|
+
module Acfs
|
8
|
+
# This represents a response. In addition to an standard HTTP
|
9
|
+
# it has a field `data` for storing the encoded body.
|
10
|
+
#
|
11
|
+
class Response
|
12
|
+
attr_accessor :data
|
13
|
+
attr_reader :headers, :body, :request, :status
|
14
|
+
|
15
|
+
include Response::Formats
|
16
|
+
include Response::Status
|
17
|
+
|
18
|
+
# delegate :status, :status_message, :success?, :modified?, :timed_out?,
|
19
|
+
# :response_body, :response_headers, :response_code, :headers,
|
20
|
+
# to: :response
|
21
|
+
|
22
|
+
def initialize(request, data = {})
|
23
|
+
@request = request
|
24
|
+
@status = data[:status] || 0
|
25
|
+
@headers = data[:headers] || {}
|
26
|
+
@body = data[:body] || ''
|
27
|
+
@data = data[:data] || nil
|
28
|
+
end
|
29
|
+
end
|
30
|
+
end
|
@@ -0,0 +1,27 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'action_dispatch'
|
4
|
+
|
5
|
+
module Acfs
|
6
|
+
class Response
|
7
|
+
# Quick accessors for format handling.
|
8
|
+
module Formats
|
9
|
+
def content_type
|
10
|
+
@content_type ||= read_content_type
|
11
|
+
end
|
12
|
+
|
13
|
+
def json?
|
14
|
+
content_type == Mime[:json]
|
15
|
+
end
|
16
|
+
|
17
|
+
private
|
18
|
+
|
19
|
+
def read_content_type
|
20
|
+
return 'text/plain' unless headers && headers['Content-Type']
|
21
|
+
|
22
|
+
content_type = headers['Content-Type'].split(/;\s*\w+="?\w+"?/).first
|
23
|
+
Mime::Type.parse(content_type).first
|
24
|
+
end
|
25
|
+
end
|
26
|
+
end
|
27
|
+
end
|
@@ -0,0 +1,33 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Acfs
|
4
|
+
class Response
|
5
|
+
# Method to fetch information about response status.
|
6
|
+
#
|
7
|
+
module Status
|
8
|
+
# Return response status code. Will return zero if
|
9
|
+
# request was not executed or failed on client side.
|
10
|
+
#
|
11
|
+
def status_code
|
12
|
+
return @status.to_i if defined? :@status
|
13
|
+
# return response.response_code unless response.nil?
|
14
|
+
# 0
|
15
|
+
end
|
16
|
+
alias code status_code
|
17
|
+
|
18
|
+
# Return true if response was successful indicated by
|
19
|
+
# response status code.
|
20
|
+
#
|
21
|
+
def success?
|
22
|
+
code >= 200 && code < 300
|
23
|
+
end
|
24
|
+
|
25
|
+
# Return true unless response status code indicates that
|
26
|
+
# resource was not modified according to send precondition headers.
|
27
|
+
#
|
28
|
+
def modified?
|
29
|
+
code != 304
|
30
|
+
end
|
31
|
+
end
|
32
|
+
end
|
33
|
+
end
|
data/lib/acfs/rspec.rb
ADDED
data/lib/acfs/runner.rb
ADDED
@@ -0,0 +1,102 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'acfs/service/middleware'
|
4
|
+
|
5
|
+
module Acfs
|
6
|
+
# @api private
|
7
|
+
#
|
8
|
+
class Runner
|
9
|
+
include Service::Middleware
|
10
|
+
attr_reader :adapter
|
11
|
+
|
12
|
+
def initialize(adapter)
|
13
|
+
@adapter = adapter
|
14
|
+
@running = false
|
15
|
+
end
|
16
|
+
|
17
|
+
# Process an operation. Synchronous operations will be run
|
18
|
+
# and parallel operations will be queued.
|
19
|
+
#
|
20
|
+
def process(op)
|
21
|
+
::ActiveSupport::Notifications.instrument 'acfs.operation.before_process', operation: op
|
22
|
+
op.synchronous? ? run(op) : enqueue(op)
|
23
|
+
end
|
24
|
+
|
25
|
+
# Run operation right now skipping queue.
|
26
|
+
#
|
27
|
+
def run(op)
|
28
|
+
::ActiveSupport::Notifications.instrument 'acfs.runner.sync_run', operation: op do
|
29
|
+
op_request(op) {|req| adapter.run req }
|
30
|
+
end
|
31
|
+
end
|
32
|
+
|
33
|
+
# List of current queued operations.
|
34
|
+
#
|
35
|
+
def queue
|
36
|
+
@queue ||= []
|
37
|
+
end
|
38
|
+
|
39
|
+
# Enqueue operation to be run later.
|
40
|
+
#
|
41
|
+
def enqueue(op)
|
42
|
+
::ActiveSupport::Notifications.instrument 'acfs.runner.enqueue', operation: op do
|
43
|
+
if running?
|
44
|
+
op_request(op) {|req| adapter.queue req }
|
45
|
+
else
|
46
|
+
queue << op
|
47
|
+
end
|
48
|
+
end
|
49
|
+
end
|
50
|
+
|
51
|
+
# Return true if queued operations are currently processed.
|
52
|
+
#
|
53
|
+
def running?
|
54
|
+
@running
|
55
|
+
end
|
56
|
+
|
57
|
+
# Start processing queued operations.
|
58
|
+
#
|
59
|
+
def start
|
60
|
+
return if running?
|
61
|
+
|
62
|
+
enqueue_operations
|
63
|
+
start_all
|
64
|
+
rescue StandardError
|
65
|
+
queue.clear
|
66
|
+
raise
|
67
|
+
end
|
68
|
+
|
69
|
+
def clear
|
70
|
+
queue.clear
|
71
|
+
adapter.abort
|
72
|
+
@running = false
|
73
|
+
end
|
74
|
+
|
75
|
+
private
|
76
|
+
|
77
|
+
def start_all
|
78
|
+
@running = true
|
79
|
+
adapter.start
|
80
|
+
ensure
|
81
|
+
@running = false
|
82
|
+
end
|
83
|
+
|
84
|
+
def enqueue_operations
|
85
|
+
while (op = queue.shift)
|
86
|
+
op_request(op) {|req| adapter.queue req }
|
87
|
+
end
|
88
|
+
end
|
89
|
+
|
90
|
+
def op_request(op)
|
91
|
+
return if Acfs::Stub.enabled? && Acfs::Stub.stubbed(op)
|
92
|
+
|
93
|
+
req = op.service.prepare op.request
|
94
|
+
return unless req.is_a? Acfs::Request
|
95
|
+
|
96
|
+
req = prepare req
|
97
|
+
return unless req.is_a? Acfs::Request
|
98
|
+
|
99
|
+
yield req
|
100
|
+
end
|
101
|
+
end
|
102
|
+
end
|
data/lib/acfs/service.rb
ADDED
@@ -0,0 +1,97 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'acfs/service/middleware'
|
4
|
+
|
5
|
+
module Acfs
|
6
|
+
# User {Acfs::Service} to define your services. That includes
|
7
|
+
# an identity used to identify the service in configuration files
|
8
|
+
# and middlewares the service uses.
|
9
|
+
#
|
10
|
+
# Configure your service URLs in a YAML file loaded in an
|
11
|
+
# initializer using the identity as a key:
|
12
|
+
#
|
13
|
+
# production:
|
14
|
+
# services:
|
15
|
+
# user_service_key: "http://users.service.org/base/path"
|
16
|
+
#
|
17
|
+
# @example
|
18
|
+
# class UserService < Acfs::Service
|
19
|
+
# identity :user_service_key
|
20
|
+
#
|
21
|
+
# use Acfs::Middleware::MessagePackDecoder
|
22
|
+
# end
|
23
|
+
#
|
24
|
+
class Service
|
25
|
+
attr_accessor :options
|
26
|
+
|
27
|
+
include Service::Middleware
|
28
|
+
|
29
|
+
# @api private
|
30
|
+
#
|
31
|
+
def initialize(options = {})
|
32
|
+
@options = options
|
33
|
+
end
|
34
|
+
|
35
|
+
# @api private
|
36
|
+
# @return [Location]
|
37
|
+
#
|
38
|
+
def location(resource_class, opts = {})
|
39
|
+
opts.reverse_merge! options
|
40
|
+
|
41
|
+
action = opts[:action] || :list
|
42
|
+
|
43
|
+
path = if opts[:path].is_a?(Hash) && opts[:path].key?(action)
|
44
|
+
opts[:path].fetch(action)
|
45
|
+
else
|
46
|
+
path = if opts[:path].is_a?(Hash)
|
47
|
+
opts[:path][:all].to_s
|
48
|
+
else
|
49
|
+
opts[:path].to_s
|
50
|
+
end
|
51
|
+
|
52
|
+
if path.blank?
|
53
|
+
path = (resource_class.name || 'class').pluralize.underscore
|
54
|
+
end
|
55
|
+
|
56
|
+
resource_class.location_default_path(action, path.strip)
|
57
|
+
end
|
58
|
+
|
59
|
+
if path.nil?
|
60
|
+
raise ArgumentError.new "Location for `#{action}' explicit disabled by set to nil."
|
61
|
+
end
|
62
|
+
|
63
|
+
Location.new [self.class.base_url.to_s, path.to_s].join('/')
|
64
|
+
end
|
65
|
+
|
66
|
+
class << self
|
67
|
+
# @api public
|
68
|
+
#
|
69
|
+
# @overload identity()
|
70
|
+
# Return configured identity key or derive key from class name.
|
71
|
+
#
|
72
|
+
# @return [Symbol] Service identity key.
|
73
|
+
#
|
74
|
+
# @overload identity(identity)
|
75
|
+
# Set identity key.
|
76
|
+
#
|
77
|
+
# @param [#to_s] identity New identity key.
|
78
|
+
# @return [Symbol] New set identity key.
|
79
|
+
#
|
80
|
+
def identity(identity = nil)
|
81
|
+
@identity = identity.to_s.to_sym unless identity.nil?
|
82
|
+
@identity ||= name.to_sym
|
83
|
+
end
|
84
|
+
|
85
|
+
# @api private
|
86
|
+
# @return [String]
|
87
|
+
#
|
88
|
+
def base_url
|
89
|
+
unless (base = Acfs::Configuration.current.locate identity)
|
90
|
+
raise ArgumentError.new "#{identity} not configured. Add `locate '#{identity.to_s.underscore}', 'http://service.url/'` to your configuration."
|
91
|
+
end
|
92
|
+
|
93
|
+
base.to_s
|
94
|
+
end
|
95
|
+
end
|
96
|
+
end
|
97
|
+
end
|
@@ -0,0 +1,58 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'acfs/service/middleware/stack'
|
4
|
+
|
5
|
+
module Acfs
|
6
|
+
class Service
|
7
|
+
# Module providing all function to register middlewares
|
8
|
+
# on services and process queued request through the
|
9
|
+
# middleware stack.
|
10
|
+
#
|
11
|
+
module Middleware
|
12
|
+
extend ActiveSupport::Concern
|
13
|
+
|
14
|
+
# @api private
|
15
|
+
# @return [Request]
|
16
|
+
#
|
17
|
+
def prepare(request)
|
18
|
+
self.class.middleware.call request
|
19
|
+
end
|
20
|
+
|
21
|
+
module ClassMethods
|
22
|
+
# @!method use(klass, *args, &block)
|
23
|
+
# @api public
|
24
|
+
#
|
25
|
+
# Register a new middleware to be used for this service.
|
26
|
+
#
|
27
|
+
# @example
|
28
|
+
# class MyService < Acfs::Service
|
29
|
+
# self.base_url = 'http://my.srv'
|
30
|
+
# use Acfs::Middleware::JSON
|
31
|
+
# end
|
32
|
+
#
|
33
|
+
# @param [Class] klass Middleware class to append
|
34
|
+
# @param [Array<Object>] args Arguments passed to klass initialize
|
35
|
+
# @param [Proc] block Block passed to klass initialize
|
36
|
+
# @return [undefined]
|
37
|
+
#
|
38
|
+
def use(klass, *args, &block)
|
39
|
+
# Backward compatible behavior
|
40
|
+
middleware.insert(0, klass, *args, &block)
|
41
|
+
end
|
42
|
+
|
43
|
+
# @api private
|
44
|
+
#
|
45
|
+
# Return top most middleware.
|
46
|
+
#
|
47
|
+
# @return [#call]
|
48
|
+
#
|
49
|
+
def middleware
|
50
|
+
@middleware ||= Stack.new
|
51
|
+
end
|
52
|
+
|
53
|
+
# @deprecated
|
54
|
+
delegate :clear, to: :middleware
|
55
|
+
end
|
56
|
+
end
|
57
|
+
end
|
58
|
+
end
|