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,46 @@
1
+ # frozen_string_literal: true
2
+
3
+ lib = File.expand_path('lib', __dir__)
4
+ $LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
5
+ require 'acfs/version'
6
+
7
+ Gem::Specification.new do |spec|
8
+ spec.name = 'acfs'
9
+ spec.version = Acfs::VERSION
10
+ spec.authors = ['Jan Graichen']
11
+ spec.email = %w[jgraichen@altimos.de]
12
+ spec.homepage = 'https://github.com/jgraichen/acfs'
13
+ spec.license = 'MIT'
14
+ spec.description = 'API Client For Services'
15
+ spec.summary = <<~SUMMARY.strip
16
+ An abstract API base client for service oriented application.
17
+ SUMMARY
18
+
19
+ spec.files = Dir['**/*'].grep(%r{
20
+ ^((bin|lib|test|spec|features)/|
21
+ .*\.gemspec|.*LICENSE.*|.*README.*|.*CHANGELOG.*)
22
+ }xi)
23
+
24
+ spec.executables = spec.files.grep(%r{^bin/}) {|f| File.basename(f) }
25
+ spec.test_files = spec.files.grep(%r{^(test|spec|features)/})
26
+ spec.require_paths = %w[lib]
27
+
28
+ spec.add_runtime_dependency 'actionpack', '>= 4.2'
29
+ spec.add_runtime_dependency 'activemodel', '>= 4.2'
30
+ spec.add_runtime_dependency 'activesupport', '>= 4.2'
31
+ spec.add_runtime_dependency 'multi_json'
32
+
33
+ # Bundle update w/o version resolves to 0.3.3 ...
34
+ spec.add_runtime_dependency 'typhoeus', '~> 1.0'
35
+
36
+ spec.add_runtime_dependency 'rack'
37
+
38
+ spec.add_development_dependency 'bundler'
39
+
40
+ if ENV['TRAVIS_BUILD_NUMBER'] && !ENV['TRAVIS_TAG']
41
+ # Append travis build number for auto-releases
42
+ # rubocop:disable Gemspec/DuplicatedAssignment
43
+ spec.version = "#{spec.version}.1.b#{ENV['TRAVIS_BUILD_NUMBER']}"
44
+ # rubocop:enable Gemspec/DuplicatedAssignment
45
+ end
46
+ end
@@ -0,0 +1,51 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'active_support'
4
+ require 'active_support/core_ext/hash'
5
+ require 'active_support/core_ext/class'
6
+ require 'active_support/core_ext/string'
7
+ require 'active_support/core_ext/module'
8
+ require 'active_support/notifications'
9
+
10
+ module Acfs
11
+ extend ActiveSupport::Autoload
12
+ require 'acfs/version'
13
+ require 'acfs/errors'
14
+ require 'acfs/global'
15
+ require 'acfs/util'
16
+
17
+ require 'acfs/collection'
18
+ require 'acfs/configuration'
19
+ require 'acfs/location'
20
+ require 'acfs/operation'
21
+ require 'acfs/request'
22
+ require 'acfs/resource'
23
+ require 'acfs/response'
24
+ require 'acfs/runner'
25
+ require 'acfs/service'
26
+ require 'acfs/singleton_resource'
27
+
28
+ extend Global
29
+
30
+ autoload :Stub
31
+
32
+ module Middleware
33
+ extend ActiveSupport::Autoload
34
+ require 'acfs/middleware/base'
35
+ require 'acfs/middleware/serializer'
36
+
37
+ autoload :Print
38
+ autoload :Logger
39
+ autoload :JSON
40
+ autoload :JsonDecoder, 'acfs/middleware/json'
41
+ autoload :JsonEncoder, 'acfs/middleware/json'
42
+ autoload :MessagePack, 'acfs/middleware/msgpack'
43
+ autoload :MessagePackDecoder, 'acfs/middleware/msgpack'
44
+ autoload :MessagePackEncoder, 'acfs/middleware/msgpack'
45
+ end
46
+
47
+ module Adapter
48
+ require 'acfs/adapter/base'
49
+ require 'acfs/adapter/typhoeus'
50
+ end
51
+ end
@@ -0,0 +1,24 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Acfs::Adapter
4
+ # Base adapter handling operation queuing
5
+ # and processing.
6
+ #
7
+ class Base
8
+ # Start processing queued requests.
9
+ #
10
+ def start; end
11
+
12
+ # Abort running and queued requests.
13
+ #
14
+ def abort; end
15
+
16
+ # Run request right now skipping queue.
17
+ #
18
+ def run(_); end
19
+
20
+ # Enqueue request to be run later.
21
+ #
22
+ def queue(_); end
23
+ end
24
+ end
@@ -0,0 +1,69 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'typhoeus'
4
+
5
+ module Acfs
6
+ module Adapter
7
+ # Adapter for Typhoeus.
8
+ #
9
+ class Typhoeus < Base
10
+ def initialize(**kwargs)
11
+ @options = kwargs
12
+ end
13
+
14
+ def start
15
+ hydra.run
16
+ rescue StandardError
17
+ @hydra = nil
18
+ raise
19
+ end
20
+
21
+ delegate :abort, to: :hydra
22
+
23
+ def run(request)
24
+ convert_request(request).run
25
+ end
26
+
27
+ def queue(request)
28
+ hydra.queue convert_request request
29
+ end
30
+
31
+ protected
32
+
33
+ def hydra
34
+ @hydra ||= ::Typhoeus::Hydra.new(**@options)
35
+ end
36
+
37
+ def convert_request(req)
38
+ request = ::Typhoeus::Request.new req.url,
39
+ method: req.method,
40
+ params: req.params,
41
+ headers: req.headers.merge(
42
+ 'Expect' => '',
43
+ 'Transfer-Encoding' => ''
44
+ ),
45
+ body: req.body
46
+
47
+ request.on_complete do |response|
48
+ if response.timed_out?
49
+ raise ::Acfs::TimeoutError.new(req)
50
+ elsif response.code.zero?
51
+ # Failed to get HTTP response
52
+ raise ::Acfs::RequestError.new(req, response.return_message)
53
+ else
54
+ req.complete! convert_response(req, response)
55
+ end
56
+ end
57
+
58
+ request
59
+ end
60
+
61
+ def convert_response(request, response)
62
+ Acfs::Response.new request,
63
+ status: response.code,
64
+ headers: response.headers,
65
+ body: response.body
66
+ end
67
+ end
68
+ end
69
+ end
@@ -0,0 +1,28 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'delegate'
4
+
5
+ require 'acfs/resource/loadable'
6
+ require 'acfs/collections/paginatable'
7
+
8
+ module Acfs
9
+ class Collection < ::Delegator
10
+ include Resource::Loadable
11
+ include Acfs::Util::Callbacks
12
+ include Collections::Paginatable
13
+
14
+ def initialize(resource_class)
15
+ super([])
16
+
17
+ @resource_class = resource_class
18
+ end
19
+
20
+ def __getobj__
21
+ @models
22
+ end
23
+
24
+ def __setobj__(obj)
25
+ @models = obj
26
+ end
27
+ end
28
+ end
@@ -0,0 +1,76 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Acfs::Collections
4
+ module Paginatable
5
+ extend ActiveSupport::Concern
6
+
7
+ included do
8
+ def self.operation(_action, opts = {}, &_block)
9
+ opts[:url]
10
+ end
11
+
12
+ attr_reader :total_pages, :current_page, :total_count
13
+ end
14
+
15
+ def process_response(response)
16
+ setup_params response.request.params if response.request
17
+ setup_headers response.headers
18
+ end
19
+
20
+ def next_page(&block)
21
+ page 'next', &block
22
+ end
23
+
24
+ def prev_page(&block)
25
+ page 'prev', &block
26
+ end
27
+
28
+ def first_page(&block)
29
+ page 'first', &block
30
+ end
31
+
32
+ def last_page(&block)
33
+ page 'last', &block
34
+ end
35
+
36
+ def page(rel, &block)
37
+ return unless relations[rel]
38
+
39
+ @resource_class.all nil, url: relations[rel], &block
40
+ end
41
+
42
+ private
43
+
44
+ def relations
45
+ @relations ||= {}
46
+ end
47
+
48
+ def setup_headers(headers)
49
+ if headers['X-Total-Pages']
50
+ @total_pages = Integer(headers['X-Total-Pages'])
51
+ end
52
+
53
+ if headers['X-Total-Count']
54
+ @total_count = Integer(headers['X-Total-Count'])
55
+ end
56
+
57
+ setup_links headers['Link'] if headers['Link']
58
+ end
59
+
60
+ def setup_links(links)
61
+ links.split(/,\s+/).each do |link|
62
+ if link =~ /^\s*<([^>]+)>.*\s+rel="([\w_-]+)".*$/
63
+ relations[Regexp.last_match[2]] = Regexp.last_match[1]
64
+ end
65
+ end
66
+ end
67
+
68
+ def setup_params(params)
69
+ @current_page = begin
70
+ Integer params.fetch(:page, 1)
71
+ rescue ArgumentError
72
+ params[:page]
73
+ end
74
+ end
75
+ end
76
+ end
@@ -0,0 +1,120 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'uri'
4
+ require 'yaml'
5
+
6
+ module Acfs
7
+ # Acfs configuration is used to locate services and get their base URLs.
8
+ #
9
+ class Configuration
10
+ attr_reader :locations
11
+ attr_accessor :adapter
12
+
13
+ # @api private
14
+ def initialize
15
+ @locations = {}
16
+ end
17
+
18
+ # @api public
19
+ #
20
+ # Configure using given block. If block accepts zero arguments
21
+ # bock will be evaluated in context of the configuration instance
22
+ # otherwise the configuration instance will be given as first arguments.
23
+ #
24
+ # @yield [configuration] Give configuration as arguments or evaluate block
25
+ # in context of configuration object.
26
+ # @yieldparam configuration [Configuration] Configuration object.
27
+ # @return [undefined]
28
+ #
29
+ def configure(&block)
30
+ if block.arity.positive?
31
+ block.call self
32
+ else
33
+ instance_eval(&block)
34
+ end
35
+ end
36
+
37
+ # @api public
38
+ #
39
+ # @overload locate(service, uri)
40
+ # Configures URL where a service can be reached.
41
+ #
42
+ # @param [Symbol] service
43
+ # Service identity key for service that is reachable under given URL.
44
+ #
45
+ # @param [String] uri
46
+ # URL where service is reachable. Will be passed to {URI.parse}.
47
+ #
48
+ # @return [undefined]
49
+ #
50
+ # @overload locate(service)
51
+ # Return configured base URL for given service identity key.
52
+ #
53
+ # @param [Symbol] service Service identity key to lookup.
54
+ # @return [URI, NilClass] Configured base URL or nil.
55
+ #
56
+ def locate(service, uri = nil)
57
+ service = service.to_s.underscore.to_sym
58
+ if uri.nil?
59
+ locations[service]
60
+ else
61
+ locations[service] = URI.parse uri
62
+ end
63
+ end
64
+
65
+ # @api public
66
+ #
67
+ # Load configuration from given YAML file.
68
+ #
69
+ # @param [String] filename Path to YAML configuration file.
70
+ # @return [undefined]
71
+ #
72
+ def load(filename)
73
+ config = YAML.safe_load(File.read(filename), [], [], true)
74
+ env = ENV['RACK_ENV'] || ENV['RAILS_ENV'] || 'development'
75
+
76
+ config = config[env] if config.key? env
77
+ config.each do |key, value|
78
+ case key
79
+ when 'services' then load_services value
80
+ end
81
+ end
82
+ end
83
+
84
+ # @api private
85
+ #
86
+ # Load services from configuration YAML.
87
+ #
88
+ def load_services(services)
89
+ services.each do |service, data|
90
+ if (val = data).is_a?(String) || (val = data['locate'])
91
+ locate service.to_sym, val
92
+ end
93
+ end
94
+ end
95
+
96
+ class << self
97
+ # @api private
98
+ #
99
+ # Return current configuration object.
100
+ #
101
+ # @return [Configuration]
102
+ #
103
+ def current
104
+ @current ||= new
105
+ end
106
+
107
+ # @api private
108
+ #
109
+ # Swap configuration object with given new one. Must be
110
+ # a {Configuration} object.
111
+ #
112
+ # @param [Configuration] configuration
113
+ # @return [undefined]
114
+ #
115
+ def set(configuration)
116
+ @current = configuration if configuration.is_a? Configuration
117
+ end
118
+ end
119
+ end
120
+ end
@@ -0,0 +1,127 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Acfs
4
+ # Acfs base error.
5
+ #
6
+ class Error < StandardError
7
+ def initialize(opts = {}, message = nil)
8
+ opts.merge! message: message if message
9
+ super opts[:message]
10
+ end
11
+ end
12
+
13
+ class UnsupportedOperation < StandardError; end
14
+
15
+ class RequestError < Error
16
+ attr_reader :request
17
+
18
+ def initialize(request, message)
19
+ @request = request
20
+
21
+ message = "#{message}: #{request.method.upcase} #{request.url}"
22
+
23
+ super message: message
24
+ end
25
+ end
26
+
27
+ class TimeoutError < RequestError
28
+ def initialize(request)
29
+ super(request, "Timeout reached")
30
+ end
31
+ end
32
+
33
+ # Response error containing the responsible response object.
34
+ #
35
+ class ErroneousResponse < Error
36
+ attr_reader :response
37
+
38
+ def initialize(opts = {})
39
+ @response = opts[:response]
40
+
41
+ if response
42
+ message = (opts[:message] ? opts[:message] + ':' : 'Received') +
43
+ " #{response.code} for #{response.request.method.upcase}" \
44
+ " #{response.request.url} #{response.request.format}"
45
+ else
46
+ message = opts[:message] || 'Received erroneous response'
47
+ end
48
+
49
+ super opts, message
50
+ end
51
+ end
52
+
53
+ class AmbiguousStubError < Error
54
+ attr_reader :stubs, :operation
55
+
56
+ def initialize(opts = {})
57
+ require 'pp'
58
+
59
+ @stubs = opts.delete :stubs
60
+ @operation = opts.delete :operation
61
+
62
+ message = "Ambiguous stubs for #{operation.action} " \
63
+ "on #{operation.resource}.\n" +
64
+ stubs.map {|s| " #{s.opts.pretty_inspect}" }.join
65
+
66
+ super opts, message
67
+ end
68
+ end
69
+
70
+ # Resource not found error raised on a 404 response
71
+ #
72
+ class ResourceNotFound < ErroneousResponse
73
+ end
74
+
75
+ class InvalidResource < ErroneousResponse
76
+ attr_reader :errors, :resource
77
+
78
+ def initialize(opts = {})
79
+ @errors = opts.delete :errors
80
+ @resource = opts.delete :resource
81
+
82
+ if @errors.is_a?(Hash)
83
+ opts[:message] ||= @errors.each_pair.map do |k, v|
84
+ @errors.is_a?(Array) ? "#{k}: #{v.join(', ')}" : "#{k}: #{v}"
85
+ end.join ', '
86
+ elsif @errors.is_a?(Array)
87
+ opts[:message] ||= @errors.join ', '
88
+ end
89
+
90
+ super
91
+ end
92
+ end
93
+
94
+ # A ResourceNotLoaded error will be thrown when calling some
95
+ # modifing methods on not loaded resources as it is usally
96
+ # unwanted to call e.g. `update_attributes` on a not loaded
97
+ # resource.
98
+ # Correct solution is to first run `Acfs.run` to fetch the
99
+ # resource and then update the resource.
100
+ #
101
+ class ResourceNotLoaded < Error
102
+ attr_reader :resource
103
+
104
+ def initialize(opts = {})
105
+ @resource = opts.delete :resource
106
+ super
107
+ end
108
+ end
109
+
110
+ # Gets raised if ressource type is no valid subclass of
111
+ # parent resource. Check if the type is set to the correct
112
+ # Acfs::Resource Name
113
+ class ResourceTypeError < Error
114
+ attr_reader :base_class
115
+ attr_reader :type_name
116
+
117
+ def initialize(opts = {})
118
+ @base_class = opts.delete :base_class
119
+ @type_name = opts.delete :type_name
120
+ opts[:message] = "Received resource type `#{type_name}` " \
121
+ "is no subclass of #{base_class}"
122
+ super
123
+ end
124
+ end
125
+
126
+ class RealRequestsNotAllowedError < StandardError; end
127
+ end