acfs 1.3.3 → 1.3.4

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