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,39 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'acfs/request/callbacks'
4
+
5
+ module Acfs
6
+ # Encapsulate all data required to make up a request to the
7
+ # underlaying http library.
8
+ #
9
+ class Request
10
+ attr_accessor :body, :format
11
+ attr_reader :url, :headers, :params, :data, :method, :operation
12
+
13
+ include Request::Callbacks
14
+
15
+ def initialize(url, options = {}, &block)
16
+ @url = URI.parse(url.to_s).tap do |_url|
17
+ @data = options.delete(:data) || nil
18
+ @format = options.delete(:format) || :json
19
+ @headers = options.delete(:headers) || {}
20
+ @params = options.delete(:params) || {}
21
+ @method = options.delete(:method) || :get
22
+ end.to_s
23
+ @operation = options.delete(:operation) || nil
24
+ on_complete(&block) if block_given?
25
+ end
26
+
27
+ def data?
28
+ !data.nil?
29
+ end
30
+
31
+ class << self
32
+ def new(*attrs)
33
+ return attrs[0] if attrs[0].is_a? self
34
+
35
+ super
36
+ end
37
+ end
38
+ end
39
+ end
@@ -0,0 +1,54 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Acfs
4
+ class Request
5
+ # Module containing callback handling for Requests.
6
+ # Current the only callback type is `on_complete`:
7
+ #
8
+ # request = Request.new 'URL'
9
+ # request.on_complete { |response| ... }
10
+ #
11
+ module Callbacks
12
+ # Add a new `on_complete` callback for this request.
13
+ #
14
+ # @example Set on_complete.
15
+ # request.on_complete { |response| print response.body }
16
+ #
17
+ # @param [ Block ] block The callback block to execute.
18
+ #
19
+ # @yield [ Acfs::Response ]
20
+ #
21
+ # @return [ Acfs::Request ] The request itself.
22
+ #
23
+ def on_complete(&block)
24
+ callbacks.insert 0, block if block_given?
25
+ self
26
+ end
27
+
28
+ # Return array of all callbacks.
29
+ #
30
+ # @return [ Array<Block> ] All callbacks.
31
+ #
32
+ def callbacks
33
+ @callbacks ||= []
34
+ end
35
+
36
+ # Trigger all callback for given response.
37
+ #
38
+ # @return [ Acfs::Request ] The request itself.
39
+ #
40
+ def complete!(response)
41
+ call_callback response, 0
42
+ self
43
+ end
44
+
45
+ private
46
+
47
+ def call_callback(res, index)
48
+ if index < callbacks.size
49
+ callbacks[index].call res, proc {|bres| call_callback bres, index + 1 }
50
+ end
51
+ end
52
+ end
53
+ end
54
+ end
@@ -0,0 +1,39 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'active_model'
4
+
5
+ # @api public
6
+ #
7
+ class Acfs::Resource
8
+ require 'acfs/resource/initialization'
9
+ require 'acfs/resource/attributes'
10
+ require 'acfs/resource/dirty'
11
+ require 'acfs/resource/loadable'
12
+ require 'acfs/resource/locatable'
13
+ require 'acfs/resource/operational'
14
+ require 'acfs/resource/persistence'
15
+ require 'acfs/resource/query_methods'
16
+ require 'acfs/resource/service'
17
+ require 'acfs/resource/validation'
18
+
19
+ if ActiveModel::VERSION::MAJOR >= 4
20
+ include ActiveModel::Model
21
+ else
22
+ extend ActiveModel::Naming
23
+ extend ActiveModel::Translation
24
+ include ActiveModel::Conversion
25
+ include ActiveModel::Validations
26
+ end
27
+
28
+ include Initialization
29
+
30
+ include Attributes
31
+ include Loadable
32
+ include Persistence
33
+ include Locatable
34
+ include Operational
35
+ include QueryMethods
36
+ include Service
37
+ include Dirty
38
+ include Validation
39
+ end
@@ -0,0 +1,269 @@
1
+ # frozen_string_literal: true
2
+
3
+ class Acfs::Resource
4
+ #
5
+ # = Acfs Attributes
6
+ #
7
+ # Allows to specify attributes of a class with default
8
+ # values and type safety.
9
+ #
10
+ # @example
11
+ # class User < Acfs::Resource
12
+ # attribute :name, :string, default: 'Anon'
13
+ # attribute :age, :integer
14
+ # attribute :special, My::Special::Type
15
+ # end
16
+ #
17
+ # For each attribute a setter and getter will be created and values will be
18
+ # type casted when set.
19
+ #
20
+ module Attributes
21
+ extend ActiveSupport::Concern
22
+ include ActiveModel::AttributeMethods
23
+
24
+ # @api public
25
+ #
26
+ # Write default attributes defined in resource class.
27
+ #
28
+ # @see #write_attributes
29
+ # @see ClassMethods#attributes
30
+ #
31
+ def initialize(*attrs)
32
+ write_attributes self.class.attributes
33
+ reset_changes
34
+ super
35
+ end
36
+
37
+ # @api public
38
+ #
39
+ # Returns ActiveModel compatible list of attributes and values.
40
+ #
41
+ # @example
42
+ # class User < Acfs::Resource
43
+ # attribute :name, type: String, default: 'Anon'
44
+ # end
45
+ # user = User.new(name: 'John')
46
+ # user.attributes # => { "name" => "John" }
47
+ #
48
+ # @return [HashWithIndifferentAccess{Symbol => Object}]
49
+ # Attributes and their values.
50
+ #
51
+ def attributes
52
+ @_attrs ||= HashWithIndifferentAccess.new
53
+ end
54
+
55
+ # @api public
56
+ #
57
+ # Update all attributes with given hash. Attribute values will be casted
58
+ # to defined attribute type.
59
+ #
60
+ # @example
61
+ # user.attributes = { :name => 'Adam' }
62
+ # user.name # => 'Adam'
63
+ #
64
+ # @param [Hash{String, Symbol => Object}, #each{|key, value|}]
65
+ # Attributes to set in resource.
66
+ # @see #write_attributes Delegates attributes hash to {#write_attributes}.
67
+ #
68
+ def attributes=(attributes)
69
+ write_attributes attributes
70
+ end
71
+
72
+ # @api public
73
+ #
74
+ # Read an attribute from instance variable.
75
+ #
76
+ # @param [Symbol, String] name Attribute name.
77
+ # @return [Object] Attribute value.
78
+ #
79
+ def read_attribute(name)
80
+ attributes[name.to_s]
81
+ end
82
+
83
+ # @api public
84
+ #
85
+ # Write a hash of attributes and values.
86
+ #
87
+ # If attribute value is a `Proc` it will be evaluated in the context
88
+ # of the resource after all non-proc attribute values are set. Values
89
+ # will be casted to defined attribute type.
90
+ #
91
+ # The behavior is used to apply default attributes from resource
92
+ # class definition.
93
+ #
94
+ # @example
95
+ # user.write_attributes name: 'john', email: ->{ "#{name}@example.org" }
96
+ # user.name # => 'john'
97
+ # user.email # => 'john@example.org'
98
+ #
99
+ # @param [Hash{String, Symbol => Object, Proc}, #each{|key, value|}]
100
+ # Attributes to write.
101
+ #
102
+ # @see #write_attribute Delegates attribute values to `#write_attribute`.
103
+ #
104
+ def write_attributes(attributes, opts = {})
105
+ unless attributes.respond_to?(:each) && attributes.respond_to?(:keys)
106
+ return false
107
+ end
108
+
109
+ if opts.fetch(:unknown, :ignore) == :raise
110
+ if (attributes.keys.map(&:to_s) - self.class.attributes.keys).any?
111
+ missing = attributes.keys - self.class.attributes.keys
112
+ missing.map!(&:inspect)
113
+ raise ArgumentError.new "Unknown attributes: #{missing.join(', ')}"
114
+ end
115
+ end
116
+
117
+ procs = {}
118
+
119
+ attributes.each do |key, _|
120
+ if attributes[key].is_a? Proc
121
+ procs[key] = attributes[key]
122
+ else
123
+ write_local_attribute key, attributes[key], opts
124
+ end
125
+ end
126
+
127
+ procs.each do |key, proc|
128
+ write_local_attribute key, instance_exec(&proc), opts
129
+ end
130
+
131
+ true
132
+ end
133
+
134
+ # @api private
135
+ #
136
+ # Check if a public getter for attribute exists that should be called to
137
+ # write it or of {#write_attribute} should be called directly. This is
138
+ # necessary as {#write_attribute} should go though setters but can also
139
+ # handle unknown attribute that will not have a generated setter method.
140
+ #
141
+ def write_local_attribute(name, value, opts = {})
142
+ method = "#{name}="
143
+ if respond_to? method, true
144
+ public_send method, value
145
+ else
146
+ write_attribute name, value, opts
147
+ end
148
+ end
149
+
150
+ # @api public
151
+ #
152
+ # Write single attribute with given value. Value will be casted
153
+ # to defined attribute type.
154
+ #
155
+ # @param [String, Symbol] name Attribute name.
156
+ # @param [Object] value Value to write.
157
+ # @raise [ArgumentError] If no attribute with given name is defined.
158
+ #
159
+ def write_attribute(name, value, opts = {})
160
+ attr_type = self.class.defined_attributes[name.to_s]
161
+ if attr_type
162
+ write_raw_attribute name, attr_type.cast(value), opts
163
+ else
164
+ write_raw_attribute name, value, opts
165
+ end
166
+ end
167
+
168
+ # @api private
169
+ #
170
+ # Write an attribute without checking type or existence or casting
171
+ # value to attributes type. Value be stored in an instance variable
172
+ # named after attribute name.
173
+ #
174
+ # @param [String, Symbol] name Attribute name.
175
+ # @param [Object] value Attribute value.
176
+ #
177
+ def write_raw_attribute(name, value, _ = {})
178
+ attributes[name.to_s] = value
179
+ end
180
+
181
+ module ClassMethods
182
+ ATTR_CLASS_BASE = '::Acfs::Resource::Attributes'
183
+
184
+ #
185
+ # @api public
186
+ #
187
+ # Define a model attribute by name and type. Will create getter and
188
+ # setter for given attribute name. Existing methods will be overridden.
189
+ #
190
+ # Available types can be found in `Acfs::Model::Attributes::*`.
191
+ #
192
+ # @example
193
+ # class User < Acfs::Resource
194
+ # attribute :name, :string, default: 'Anon'
195
+ # attribute :email, :string, default: lambda{ "#{name}@example.org"}
196
+ # end
197
+ #
198
+ # @param [#to_sym] name Attribute name.
199
+ # @param [Symbol, String, Class] type Attribute
200
+ # type identifier or type class.
201
+ #
202
+ def attribute(name, type, opts = {})
203
+ if type.is_a?(Symbol) || type.is_a?(String)
204
+ type = "#{ATTR_CLASS_BASE}::#{type.to_s.classify}".constantize
205
+ end
206
+
207
+ define_attribute name.to_sym, type, opts
208
+ end
209
+
210
+ # @api public
211
+ #
212
+ # Return list of possible attributes and default
213
+ # values for this model class.
214
+ #
215
+ # @example
216
+ # class User < Acfs::Resource
217
+ # attribute :name, :string
218
+ # attribute :age, :integer, default: 25
219
+ # end
220
+ # User.attributes # => { "name": nil, "age": 25 }
221
+ #
222
+ # @return [Hash{String => Object, Proc}]
223
+ # Attributes with default values.
224
+ #
225
+ def attributes
226
+ defined_attributes.each_with_object({}) do |(key, attr), hash|
227
+ hash[key] = attr.default_value
228
+ end
229
+ end
230
+
231
+ def defined_attributes
232
+ if superclass.respond_to?(:defined_attributes)
233
+ superclass.defined_attributes.merge(local_attributes)
234
+ else
235
+ local_attributes
236
+ end
237
+ end
238
+
239
+ private
240
+
241
+ def local_attributes
242
+ @local_attributes ||= {}
243
+ end
244
+
245
+ def define_attribute(name, type, opts = {})
246
+ name = name.to_s
247
+ attribute = type.new opts
248
+
249
+ local_attributes[name] = attribute
250
+ define_attribute_method name
251
+
252
+ send :define_method, name do
253
+ read_attribute name
254
+ end
255
+
256
+ send :define_method, :"#{name}=" do |value|
257
+ write_attribute name, value
258
+ end
259
+ end
260
+ end
261
+ end
262
+ end
263
+
264
+ # Load attribute type classes.
265
+ #
266
+ Dir[File.dirname(__FILE__) + '/attributes/*.rb'].sort.each do |path|
267
+ filename = File.basename(path)
268
+ require "acfs/resource/attributes/#{filename}"
269
+ end
@@ -0,0 +1,29 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Acfs::Resource::Attributes
4
+ class Base
5
+ attr_reader :default
6
+
7
+ def initialize(default: nil)
8
+ @default = default
9
+ end
10
+
11
+ def cast(value)
12
+ cast_value(value) unless value.nil?
13
+ end
14
+
15
+ def default_value
16
+ if default.respond_to? :call
17
+ default
18
+ else
19
+ cast default
20
+ end
21
+ end
22
+
23
+ private
24
+
25
+ def cast_value(_value)
26
+ raise NotImplementedError
27
+ end
28
+ end
29
+ end
@@ -0,0 +1,39 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Acfs::Resource::Attributes
4
+ # @api public
5
+ #
6
+ # Boolean attribute type. Use it in your model as an attribute type:
7
+ #
8
+ # @example
9
+ # class User < Acfs::Resource
10
+ # attribute :name, :boolean
11
+ # end
12
+ #
13
+ # Given objects will be converted to string. The following strings
14
+ # are considered true, everything else false:
15
+ #
16
+ # true, on, yes
17
+ #
18
+ class Boolean < Base
19
+ FALSE_VALUES = [false, 0, '0', 'f', 'F', 'false', 'FALSE', 'off', 'OFF', 'no', 'NO'].to_set
20
+
21
+ # @api public
22
+ #
23
+ # Cast given object to boolean.
24
+ #
25
+ # @param [Object] value Object to cast.
26
+ # @return [TrueClass, FalseClass] Casted boolean.
27
+ #
28
+ def cast_value(value)
29
+ return true if value == true
30
+ return false if value == false
31
+
32
+ if value.blank?
33
+ nil
34
+ else
35
+ !FALSE_VALUES.include?(value)
36
+ end
37
+ end
38
+ end
39
+ end