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,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
|