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