acfs 1.3.3 → 1.3.4

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 +339 -0
  3. data/LICENSE +22 -0
  4. data/README.md +335 -0
  5. data/acfs.gemspec +46 -0
  6. data/lib/acfs.rb +51 -0
  7. data/lib/acfs/adapter/base.rb +24 -0
  8. data/lib/acfs/adapter/typhoeus.rb +69 -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 +127 -0
  13. data/lib/acfs/global.rb +101 -0
  14. data/lib/acfs/location.rb +82 -0
  15. data/lib/acfs/middleware/base.rb +24 -0
  16. data/lib/acfs/middleware/json.rb +29 -0
  17. data/lib/acfs/middleware/logger.rb +25 -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 +83 -0
  22. data/lib/acfs/request.rb +39 -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 +269 -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 +132 -0
  39. data/lib/acfs/resource/operational.rb +23 -0
  40. data/lib/acfs/resource/persistence.rb +260 -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 +39 -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 +97 -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 +194 -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 +65 -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 +181 -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 +43 -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 +136 -3
@@ -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,82 @@
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, :args
10
+
11
+ REGEXP = /^:([A-z][A-z0-9_]*)$/.freeze
12
+
13
+ def initialize(uri, args = {})
14
+ @raw = URI.parse uri
15
+ @args = args
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(args = {})
21
+ unless args.is_a?(Hash)
22
+ raise ArgumentError.new "URI path arguments must be a hash, `#{args.inspect}' given."
23
+ end
24
+
25
+ self.class.new raw.to_s, args.merge(self.args)
26
+ end
27
+
28
+ def extract_from(*args)
29
+ args = {}.tap do |collect|
30
+ arguments.each {|key| collect[key] = extract_arg key, args }
31
+ end
32
+
33
+ build args
34
+ end
35
+
36
+ def str
37
+ uri = raw.dup
38
+ uri.path = '/' + struct.map {|s| lookup_arg(s, args) }.join('/')
39
+ uri.to_s
40
+ end
41
+
42
+ def raw_uri
43
+ raw.to_s
44
+ end
45
+ alias to_s raw_uri
46
+
47
+ private
48
+
49
+ def extract_arg(key, hashes)
50
+ hashes.each_with_index do |hash, index|
51
+ if hash.key?(key)
52
+ return (index == 0 ? hash.delete(key) : hash.fetch(key))
53
+ end
54
+ end
55
+
56
+ nil
57
+ end
58
+
59
+ def lookup_arg(arg, args)
60
+ arg.is_a?(Symbol) ? lookup_replacement(arg, args) : arg
61
+ end
62
+
63
+ def lookup_replacement(sym, args)
64
+ value = get_replacement(sym, args).to_s
65
+ return ::URI.encode_www_form_component(value) unless value.empty?
66
+
67
+ raise ArgumentError.new "Cannot replace path argument `#{sym}' with empty string."
68
+ end
69
+
70
+ def get_replacement(sym, args)
71
+ args.fetch(sym.to_s) do
72
+ args.fetch(sym) do
73
+ if args[:raise].nil? || args[:raise]
74
+ raise ArgumentError.new "URI path argument `#{sym}' missing on `#{self}'. Given: `#{args}.inspect'"
75
+ else
76
+ ":#{sym}"
77
+ end
78
+ end
79
+ end
80
+ end
81
+ end
82
+ 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, options = {})
12
+ @app = app
13
+ @options = options
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,29 @@
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
+ end
21
+ end
22
+
23
+ # @deprecated
24
+ JsonDecoder = JSON
25
+
26
+ # @deprecated
27
+ JsonEncoder = JSON
28
+ end
29
+ end
@@ -0,0 +1,25 @@
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
+ def initialize(app, options = {})
11
+ super
12
+ @logger = options[:logger] if options[:logger]
13
+ end
14
+
15
+ def response(res, nxt)
16
+ logger.info "[ACFS] #{res.request.method.to_s.upcase} #{res.request.url} -> #{res.status}"
17
+ nxt.call res
18
+ end
19
+
20
+ def logger
21
+ @logger ||= ::Logger.new STDOUT
22
+ end
23
+ end
24
+ end
25
+ 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,83 @@
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
+ delegate :service, to: :resource
12
+ delegate :call, to: :callback
13
+
14
+ def initialize(resource, action, opts = {}, &block)
15
+ @resource = resource
16
+ @action = action.to_sym
17
+
18
+ # Operations can be delayed so dup params and data to avoid
19
+ # later in-place changes by modifying passed hash
20
+ @params = (opts[:params] || {}).dup
21
+ @data = (opts[:data] || {}).dup
22
+
23
+ if opts[:url]
24
+ @url = opts[:url]
25
+ else
26
+ @location = resource.location(action: @action).extract_from(@params, @data)
27
+ @url = location.str
28
+ end
29
+
30
+ @callback = block
31
+ end
32
+
33
+ def single?
34
+ %i[read update delete].include? action
35
+ end
36
+
37
+ def synchronous?
38
+ %i[update delete create].include? action
39
+ end
40
+
41
+ def id
42
+ # TODO
43
+ @id ||= params.delete(:id) || data[:id]
44
+ end
45
+
46
+ def full_params
47
+ (id ? params.merge(id: id) : params).merge location_args
48
+ end
49
+
50
+ def location_args
51
+ location ? location.args : {}
52
+ end
53
+
54
+ def method
55
+ {read: :get, list: :get, update: :put, create: :post, delete: :delete}[action]
56
+ end
57
+
58
+ def request
59
+ request = ::Acfs::Request.new url, method: method, params: params,
60
+ data: data, operation: self
61
+ request.on_complete do |response|
62
+ ::ActiveSupport::Notifications.instrument 'acfs.operation.complete',
63
+ operation: self,
64
+ response: response
65
+
66
+ handle_failure response unless response.success?
67
+ callback.call response.data, response
68
+ end
69
+ request
70
+ end
71
+
72
+ def handle_failure(response)
73
+ case response.code
74
+ when 404
75
+ raise ::Acfs::ResourceNotFound.new response: response
76
+ when 422
77
+ raise ::Acfs::InvalidResource.new response: response, errors: response.data.try(:[], 'errors')
78
+ else
79
+ raise ::Acfs::ErroneousResponse.new response: response
80
+ end
81
+ end
82
+ end
83
+ end