acfs 1.3.3 → 1.6.0

Sign up to get free protection for your applications and to get access to all the features.
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