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.
Files changed (96) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +372 -0
  3. data/LICENSE +22 -0
  4. data/README.md +321 -0
  5. data/acfs.gemspec +38 -0
  6. data/lib/acfs.rb +51 -0
  7. data/lib/acfs/adapter/base.rb +26 -0
  8. data/lib/acfs/adapter/typhoeus.rb +82 -0
  9. data/lib/acfs/collection.rb +28 -0
  10. data/lib/acfs/collections/paginatable.rb +76 -0
  11. data/lib/acfs/configuration.rb +120 -0
  12. data/lib/acfs/errors.rb +147 -0
  13. data/lib/acfs/global.rb +101 -0
  14. data/lib/acfs/location.rb +76 -0
  15. data/lib/acfs/middleware/base.rb +24 -0
  16. data/lib/acfs/middleware/json.rb +31 -0
  17. data/lib/acfs/middleware/logger.rb +23 -0
  18. data/lib/acfs/middleware/msgpack.rb +32 -0
  19. data/lib/acfs/middleware/print.rb +23 -0
  20. data/lib/acfs/middleware/serializer.rb +41 -0
  21. data/lib/acfs/operation.rb +96 -0
  22. data/lib/acfs/request.rb +32 -0
  23. data/lib/acfs/request/callbacks.rb +54 -0
  24. data/lib/acfs/resource.rb +39 -0
  25. data/lib/acfs/resource/attributes.rb +270 -0
  26. data/lib/acfs/resource/attributes/base.rb +29 -0
  27. data/lib/acfs/resource/attributes/boolean.rb +39 -0
  28. data/lib/acfs/resource/attributes/date_time.rb +32 -0
  29. data/lib/acfs/resource/attributes/dict.rb +39 -0
  30. data/lib/acfs/resource/attributes/float.rb +33 -0
  31. data/lib/acfs/resource/attributes/integer.rb +29 -0
  32. data/lib/acfs/resource/attributes/list.rb +36 -0
  33. data/lib/acfs/resource/attributes/string.rb +26 -0
  34. data/lib/acfs/resource/attributes/uuid.rb +48 -0
  35. data/lib/acfs/resource/dirty.rb +37 -0
  36. data/lib/acfs/resource/initialization.rb +31 -0
  37. data/lib/acfs/resource/loadable.rb +35 -0
  38. data/lib/acfs/resource/locatable.rb +135 -0
  39. data/lib/acfs/resource/operational.rb +26 -0
  40. data/lib/acfs/resource/persistence.rb +258 -0
  41. data/lib/acfs/resource/query_methods.rb +266 -0
  42. data/lib/acfs/resource/service.rb +44 -0
  43. data/lib/acfs/resource/validation.rb +49 -0
  44. data/lib/acfs/response.rb +30 -0
  45. data/lib/acfs/response/formats.rb +27 -0
  46. data/lib/acfs/response/status.rb +33 -0
  47. data/lib/acfs/rspec.rb +13 -0
  48. data/lib/acfs/runner.rb +102 -0
  49. data/lib/acfs/service.rb +94 -0
  50. data/lib/acfs/service/middleware.rb +58 -0
  51. data/lib/acfs/service/middleware/stack.rb +65 -0
  52. data/lib/acfs/singleton_resource.rb +85 -0
  53. data/lib/acfs/stub.rb +199 -0
  54. data/lib/acfs/util.rb +22 -0
  55. data/lib/acfs/version.rb +16 -0
  56. data/lib/acfs/yard.rb +6 -0
  57. data/spec/acfs/adapter/typhoeus_spec.rb +55 -0
  58. data/spec/acfs/collection_spec.rb +157 -0
  59. data/spec/acfs/configuration_spec.rb +53 -0
  60. data/spec/acfs/global_spec.rb +140 -0
  61. data/spec/acfs/location_spec.rb +25 -0
  62. data/spec/acfs/middleware/json_spec.rb +79 -0
  63. data/spec/acfs/middleware/msgpack_spec.rb +62 -0
  64. data/spec/acfs/operation_spec.rb +12 -0
  65. data/spec/acfs/request/callbacks_spec.rb +48 -0
  66. data/spec/acfs/request_spec.rb +79 -0
  67. data/spec/acfs/resource/attributes/boolean_spec.rb +58 -0
  68. data/spec/acfs/resource/attributes/date_time_spec.rb +51 -0
  69. data/spec/acfs/resource/attributes/dict_spec.rb +77 -0
  70. data/spec/acfs/resource/attributes/float_spec.rb +61 -0
  71. data/spec/acfs/resource/attributes/integer_spec.rb +36 -0
  72. data/spec/acfs/resource/attributes/list_spec.rb +60 -0
  73. data/spec/acfs/resource/attributes/uuid_spec.rb +42 -0
  74. data/spec/acfs/resource/attributes_spec.rb +179 -0
  75. data/spec/acfs/resource/dirty_spec.rb +49 -0
  76. data/spec/acfs/resource/initialization_spec.rb +36 -0
  77. data/spec/acfs/resource/loadable_spec.rb +22 -0
  78. data/spec/acfs/resource/locatable_spec.rb +118 -0
  79. data/spec/acfs/resource/persistance_spec.rb +322 -0
  80. data/spec/acfs/resource/query_methods_spec.rb +548 -0
  81. data/spec/acfs/resource/validation_spec.rb +129 -0
  82. data/spec/acfs/response/formats_spec.rb +52 -0
  83. data/spec/acfs/response/status_spec.rb +71 -0
  84. data/spec/acfs/runner_spec.rb +95 -0
  85. data/spec/acfs/service/middleware_spec.rb +35 -0
  86. data/spec/acfs/service_spec.rb +48 -0
  87. data/spec/acfs/singleton_resource_spec.rb +17 -0
  88. data/spec/acfs/stub_spec.rb +345 -0
  89. data/spec/acfs_spec.rb +205 -0
  90. data/spec/fixtures/config.yml +14 -0
  91. data/spec/spec_helper.rb +42 -0
  92. data/spec/support/hash.rb +11 -0
  93. data/spec/support/response.rb +12 -0
  94. data/spec/support/service.rb +92 -0
  95. data/spec/support/shared/find_callbacks.rb +50 -0
  96. metadata +159 -26
@@ -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,49 @@
1
+ # frozen_string_literal: true
2
+
3
+ class Acfs::Resource
4
+ module Validation
5
+ def remote_errors
6
+ @remote_errors ||= ActiveModel::Errors.new self
7
+ end
8
+
9
+ def remote_errors=(errors)
10
+ if errors.respond_to?(:each_pair)
11
+ errors.each_pair do |field, errs|
12
+ Array(errs).each do |err|
13
+ self.errors.add field.to_sym, err
14
+ remote_errors.add field.to_sym, err
15
+ end
16
+ end
17
+ else
18
+ Array(errors).each do |err|
19
+ self.errors.add :base, err
20
+ remote_errors.add :base, err
21
+ end
22
+ end
23
+ end
24
+
25
+ def save!(**kwargs)
26
+ unless valid?(new? ? :create : :save)
27
+ raise ::Acfs::InvalidResource.new resource: self, errors: errors.to_a
28
+ end
29
+
30
+ super
31
+ end
32
+
33
+ if ::ActiveModel.version >= Gem::Version.new('6.1')
34
+ def valid?(*args)
35
+ super
36
+
37
+ remote_errors.each {|e| errors.add(e.attribute, e.message) }
38
+ errors.empty?
39
+ end
40
+ else
41
+ def valid?(*args)
42
+ super
43
+
44
+ remote_errors.each {|f, e| errors.add(f, e) }
45
+ errors.empty?
46
+ end
47
+ end
48
+ end
49
+ 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, **opts)
23
+ @request = request
24
+ @status = opts[:status] || 0
25
+ @headers = opts[:headers] || {}
26
+ @body = opts[:body] || ''
27
+ @data = opts[: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
@@ -0,0 +1,13 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'acfs'
4
+
5
+ RSpec.configure do |config|
6
+ config.before(:each) do
7
+ Acfs::Stub.enable
8
+ end
9
+
10
+ config.after(:each) do
11
+ Acfs.reset
12
+ end
13
+ end
@@ -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
@@ -0,0 +1,94 @@
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, path: nil, action: :list, **)
39
+ path ||= options[:path]
40
+
41
+ if path.is_a?(Hash) && path.key?(action)
42
+ path = path.fetch(action)
43
+ else
44
+ path = path.is_a?(Hash) ? path[:all].to_s : path.to_s
45
+
46
+ if path.blank?
47
+ path = (resource_class.name || 'class').pluralize.underscore
48
+ end
49
+
50
+ path = resource_class.location_default_path(action, path.strip)
51
+ end
52
+
53
+ if path.nil?
54
+ raise ArgumentError.new "Location for `#{action}' explicit disabled by set to nil."
55
+ end
56
+
57
+ Location.new [self.class.base_url.to_s, path.to_s].join('/')
58
+ end
59
+
60
+ class << self
61
+ # @api public
62
+ #
63
+ # @overload identity()
64
+ # Return configured identity key or derive key from class name.
65
+ #
66
+ # @return [Symbol] Service identity key.
67
+ #
68
+ # @overload identity(identity)
69
+ # Set identity key.
70
+ #
71
+ # @param [#to_s] identity New identity key.
72
+ # @return [Symbol] New set identity key.
73
+ #
74
+ def identity(identity = nil)
75
+ @identity = identity.to_s.to_sym unless identity.nil?
76
+ @identity ||= name.to_sym
77
+ end
78
+
79
+ # @api private
80
+ # @return [String]
81
+ #
82
+ def base_url
83
+ unless (base = Acfs::Configuration.current.locate identity)
84
+ raise ArgumentError.new \
85
+ "#{identity} not configured. Add `locate '" \
86
+ "#{identity.to_s.underscore}', 'http://service.url/'` " \
87
+ 'to your configuration.'
88
+ end
89
+
90
+ base.to_s
91
+ end
92
+ end
93
+ end
94
+ 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