alba 1.1.0 → 1.5.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,33 @@
1
+ module Alba
2
+ # This module creates key transform functions
3
+ module KeyTransformFactory
4
+ class << self
5
+ # Create key transform function for given transform_type
6
+ #
7
+ # @param transform_type [Symbol] transform type
8
+ # @return [Proc] transform function
9
+ # @raise [Alba::Error] when transform_type is not supported
10
+ def create(transform_type)
11
+ case transform_type
12
+ when :camel
13
+ ->(key) { _inflector.camelize(key) }
14
+ when :lower_camel
15
+ ->(key) { _inflector.camelize_lower(key) }
16
+ when :dash
17
+ ->(key) { _inflector.dasherize(key) }
18
+ else
19
+ raise ::Alba::Error, "Unknown transform_type: #{transform_type}. Supported transform_type are :camel, :lower_camel and :dash."
20
+ end
21
+ end
22
+
23
+ private
24
+
25
+ def _inflector
26
+ Alba.inflector || begin
27
+ require_relative './default_inflector'
28
+ Alba::DefaultInflector
29
+ end
30
+ end
31
+ end
32
+ end
33
+ end
data/lib/alba/many.rb CHANGED
@@ -15,7 +15,7 @@ module Alba
15
15
  return if @object.nil?
16
16
 
17
17
  @resource = constantize(@resource)
18
- @object.map { |o| @resource.new(o, params: params, within: within).to_hash }
18
+ @resource.new(@object, params: params, within: within).to_hash
19
19
  end
20
20
  end
21
21
  end
data/lib/alba/resource.rb CHANGED
@@ -1,14 +1,20 @@
1
1
  require_relative 'one'
2
2
  require_relative 'many'
3
+ require_relative 'key_transform_factory'
4
+ require_relative 'typed_attribute'
5
+ require_relative 'deprecation'
3
6
 
4
7
  module Alba
5
8
  # This module represents what should be serialized
6
9
  module Resource
7
10
  # @!parse include InstanceMethods
8
11
  # @!parse extend ClassMethods
9
- DSLS = {_attributes: {}, _key: nil, _transform_keys: nil, _on_error: nil}.freeze
12
+ DSLS = {_attributes: {}, _key: nil, _key_for_collection: nil, _meta: nil, _transform_key_function: nil, _transforming_root_key: false, _on_error: nil, _on_nil: nil, _layout: nil}.freeze # rubocop:disable Layout/LineLength
10
13
  private_constant :DSLS
11
14
 
15
+ WITHIN_DEFAULT = Object.new.freeze
16
+ private_constant :WITHIN_DEFAULT
17
+
12
18
  # @private
13
19
  def self.included(base)
14
20
  super
@@ -28,8 +34,8 @@ module Alba
28
34
 
29
35
  # @param object [Object] the object to be serialized
30
36
  # @param params [Hash] user-given Hash for arbitrary data
31
- # @param within [Hash] determines what associations to be serialized. If not set, it serializes all associations.
32
- def initialize(object, params: {}, within: true)
37
+ # @param within [Object, nil, false, true] determines what associations to be serialized. If not set, it serializes all associations.
38
+ def initialize(object, params: {}, within: WITHIN_DEFAULT)
33
39
  @object = object
34
40
  @params = params.freeze
35
41
  @within = within
@@ -38,13 +44,22 @@ module Alba
38
44
 
39
45
  # Serialize object into JSON string
40
46
  #
41
- # @param key [Symbol]
47
+ # @param key [Symbol, nil, true] DEPRECATED, use root_key instead
48
+ # @param root_key [Symbol, nil, true]
49
+ # @param meta [Hash] metadata for this seialization
42
50
  # @return [String] serialized JSON string
43
- def serialize(key: nil)
44
- key = key.nil? ? _key : key
45
- hash = key && key != '' ? {key.to_s => serializable_hash} : serializable_hash
46
- Alba.encoder.call(hash)
51
+ def serialize(key: nil, root_key: nil, meta: {})
52
+ Alba::Deprecation.warn '`key` option to `serialize` method is deprecated, use `root_key` instead.' if key
53
+ key = key.nil? && root_key.nil? ? fetch_key : root_key || key
54
+ hash = if key && key != ''
55
+ h = {key.to_s => serializable_hash}
56
+ hash_with_metadata(h, meta)
57
+ else
58
+ serializable_hash
59
+ end
60
+ serialize_with(hash)
47
61
  end
62
+ alias to_json serialize
48
63
 
49
64
  # A Hash for serialization
50
65
  #
@@ -56,27 +71,63 @@ module Alba
56
71
 
57
72
  private
58
73
 
74
+ attr_reader :serialized_json # Mainly for layout
75
+
76
+ def encode(hash)
77
+ Alba.encoder.call(hash)
78
+ end
79
+
80
+ def serialize_with(hash)
81
+ @serialized_json = encode(hash)
82
+ case @_layout
83
+ when String # file
84
+ ERB.new(File.read(@_layout)).result(binding)
85
+ when Proc # inline
86
+ inline = instance_eval(&@_layout)
87
+ inline.is_a?(Hash) ? encode(inline) : inline
88
+ else # no layout
89
+ @serialized_json
90
+ end
91
+ end
92
+
93
+ def hash_with_metadata(hash, meta)
94
+ base = @_meta ? instance_eval(&@_meta) : {}
95
+ metadata = base.merge(meta)
96
+ hash[:meta] = metadata unless metadata.empty?
97
+ hash
98
+ end
99
+
100
+ def fetch_key
101
+ collection? ? _key_for_collection : _key
102
+ end
103
+
104
+ def _key_for_collection
105
+ return @_key_for_collection.to_s unless @_key_for_collection == true && Alba.inferring
106
+
107
+ key = resource_name.pluralize
108
+ transforming_root_key? ? transform_key(key) : key
109
+ end
110
+
59
111
  # @return [String]
60
112
  def _key
61
- if @_key == true && Alba.inferring
62
- demodulized = ActiveSupport::Inflector.demodulize(self.class.name)
63
- meth = collection? ? :tableize : :singularize
64
- ActiveSupport::Inflector.public_send(meth, demodulized.delete_suffix('Resource').downcase)
65
- else
66
- @_key.to_s
67
- end
113
+ return @_key.to_s unless @_key == true && Alba.inferring
114
+
115
+ transforming_root_key? ? transform_key(resource_name) : resource_name
116
+ end
117
+
118
+ def resource_name
119
+ self.class.name.demodulize.delete_suffix('Resource').underscore
120
+ end
121
+
122
+ def transforming_root_key?
123
+ @_transforming_root_key.nil? ? Alba.transforming_root_key : @_transforming_root_key
68
124
  end
69
125
 
70
126
  def converter
71
127
  lambda do |object|
72
128
  arrays = @_attributes.map do |key, attribute|
73
- key = transform_key(key)
74
- if attribute.is_a?(Array) # Conditional
75
- conditional_attribute(object, key, attribute)
76
- else
77
- [key, fetch_attribute(object, attribute)]
78
- end
79
- rescue ::Alba::Error, FrozenError
129
+ key_and_attribute_body_from(object, key, attribute)
130
+ rescue ::Alba::Error, FrozenError, TypeError
80
131
  raise
81
132
  rescue StandardError => e
82
133
  handle_error(e, object, key, attribute)
@@ -85,33 +136,50 @@ module Alba
85
136
  end
86
137
  end
87
138
 
139
+ def key_and_attribute_body_from(object, key, attribute)
140
+ key = transform_key(key)
141
+ if attribute.is_a?(Array) # Conditional
142
+ conditional_attribute(object, key, attribute)
143
+ else
144
+ fetched_attribute = fetch_attribute(object, key, attribute)
145
+ [key, fetched_attribute]
146
+ end
147
+ end
148
+
88
149
  def conditional_attribute(object, key, attribute)
89
150
  condition = attribute.last
151
+ if condition.is_a?(Proc)
152
+ conditional_attribute_with_proc(object, key, attribute.first, condition)
153
+ else
154
+ conditional_attribute_with_symbol(object, key, attribute.first, condition)
155
+ end
156
+ end
157
+
158
+ def conditional_attribute_with_proc(object, key, attribute, condition)
90
159
  arity = condition.arity
91
- return [] if arity <= 1 && !condition.call(object)
160
+ # We can return early to skip fetch_attribute
161
+ return [] if arity <= 1 && !instance_exec(object, &condition)
92
162
 
93
- fetched_attribute = fetch_attribute(object, attribute.first)
94
- attr = if attribute.first.is_a?(Alba::Association)
95
- attribute.first.object
96
- else
97
- fetched_attribute
98
- end
99
- return [] if arity >= 2 && !condition.call(object, attr)
163
+ fetched_attribute = fetch_attribute(object, key, attribute)
164
+ attr = attribute.is_a?(Alba::Association) ? attribute.object : fetched_attribute
165
+ return [] if arity >= 2 && !instance_exec(object, attr, &condition)
100
166
 
101
167
  [key, fetched_attribute]
102
168
  end
103
169
 
170
+ def conditional_attribute_with_symbol(object, key, attribute, condition)
171
+ return [] unless __send__(condition)
172
+
173
+ [key, fetch_attribute(object, key, attribute)]
174
+ end
175
+
104
176
  def handle_error(error, object, key, attribute)
105
177
  on_error = @_on_error || Alba._on_error
106
178
  case on_error
107
- when :raise, nil
108
- raise
109
- when :nullify
110
- [key, nil]
111
- when :ignore
112
- []
113
- when Proc
114
- on_error.call(error, object, key, attribute, self.class)
179
+ when :raise, nil then raise
180
+ when :nullify then [key, nil]
181
+ when :ignore then []
182
+ when Proc then on_error.call(error, object, key, attribute, self.class)
115
183
  else
116
184
  raise ::Alba::Error, "Unknown on_error: #{on_error.inspect}"
117
185
  end
@@ -119,40 +187,39 @@ module Alba
119
187
 
120
188
  # Override this method to supply custom key transform method
121
189
  def transform_key(key)
122
- return key unless @_transform_keys
190
+ return key if @_transform_key_function.nil?
123
191
 
124
- require_relative 'key_transformer'
125
- KeyTransformer.transform(key, @_transform_keys)
192
+ @_transform_key_function.call(key.to_s)
126
193
  end
127
194
 
128
- def fetch_attribute(object, attribute)
129
- case attribute
130
- when Symbol
131
- object.public_send attribute
132
- when Proc
133
- instance_exec(object, &attribute)
134
- when Alba::One, Alba::Many
135
- within = check_within
136
- return unless within
195
+ def fetch_attribute(object, key, attribute)
196
+ value = case attribute
197
+ when Symbol then object.public_send attribute
198
+ when Proc then instance_exec(object, &attribute)
199
+ when Alba::One, Alba::Many then yield_if_within(attribute.name.to_sym) { |within| attribute.to_hash(object, params: params, within: within) }
200
+ when TypedAttribute then attribute.value(object)
201
+ else
202
+ raise ::Alba::Error, "Unsupported type of attribute: #{attribute.class}"
203
+ end
204
+ value.nil? && nil_handler ? instance_exec(object, key, attribute, &nil_handler) : value
205
+ end
137
206
 
138
- attribute.to_hash(object, params: params, within: within)
139
- else
140
- raise ::Alba::Error, "Unsupported type of attribute: #{attribute.class}"
141
- end
207
+ def nil_handler
208
+ @nil_handler ||= (@_on_nil || Alba._on_nil)
142
209
  end
143
210
 
144
- def check_within
211
+ def yield_if_within(association_name)
212
+ within = check_within(association_name)
213
+ yield(within) if within
214
+ end
215
+
216
+ def check_within(association_name)
145
217
  case @within
146
- when Hash # Traverse within tree
147
- @within.fetch(_key.to_sym, nil)
148
- when Array # within tree ends with Array
149
- @within.find { |item| item.to_sym == _key.to_sym } # Check if at least one item in the array matches current resource
150
- when Symbol # within tree could end with Symbol
151
- @within == _key.to_sym # Check if the symbol matches current resource
152
- when true # In this case, Alba serializes all associations.
153
- true
154
- when nil, false # In these cases, Alba stops serialization here.
155
- false
218
+ when WITHIN_DEFAULT then WITHIN_DEFAULT # Default value, doesn't check within tree
219
+ when Hash then @within.fetch(association_name, nil) # Traverse within tree
220
+ when Array then @within.find { |item| item.to_sym == association_name }
221
+ when Symbol then @within == association_name
222
+ when nil, true, false then false # Stop here
156
223
  else
157
224
  raise Alba::Error, "Unknown type for within option: #{@within.class}"
158
225
  end
@@ -173,23 +240,49 @@ module Alba
173
240
  DSLS.each_key { |name| subclass.instance_variable_set("@#{name}", instance_variable_get("@#{name}").clone) }
174
241
  end
175
242
 
243
+ # Defining methods for DSLs and disable parameter number check since for users' benefits increasing params is fine
244
+ # rubocop:disable Metrics/ParameterLists
245
+
176
246
  # Set multiple attributes at once
177
247
  #
178
248
  # @param attrs [Array<String, Symbol>]
179
- # @param options [Hash] option hash including `if` that is a condition to render these attributes
180
- def attributes(*attrs, **options)
249
+ # @param if [Proc] condition to decide if it should serialize these attributes
250
+ # @param attrs_with_types [Hash<[Symbol, String], [Array<Symbol, Proc>, Symbol]>]
251
+ # attributes with name in its key and type and optional type converter in its value
252
+ # @return [void]
253
+ def attributes(*attrs, if: nil, **attrs_with_types) # rubocop:disable Naming/MethodParameterName
254
+ if_value = binding.local_variable_get(:if)
255
+ assign_attributes(attrs, if_value)
256
+ assign_attributes_with_types(attrs_with_types, if_value)
257
+ end
258
+
259
+ def assign_attributes(attrs, if_value)
181
260
  attrs.each do |attr_name|
182
- attr = options[:if] ? [attr_name.to_sym, options[:if]] : attr_name.to_sym
261
+ attr = if_value ? [attr_name.to_sym, if_value] : attr_name.to_sym
183
262
  @_attributes[attr_name.to_sym] = attr
184
263
  end
185
264
  end
265
+ private :assign_attributes
266
+
267
+ def assign_attributes_with_types(attrs_with_types, if_value)
268
+ attrs_with_types.each do |attr_name, type_and_converter|
269
+ attr_name = attr_name.to_sym
270
+ type, type_converter = type_and_converter
271
+ typed_attr = TypedAttribute.new(name: attr_name, type: type, converter: type_converter)
272
+ attr = if_value ? [typed_attr, if_value] : typed_attr
273
+ @_attributes[attr_name] = attr
274
+ end
275
+ end
276
+ private :assign_attributes_with_types
186
277
 
187
278
  # Set an attribute with the given block
188
279
  #
189
280
  # @param name [String, Symbol] key name
190
- # @param options [Hash] option hash including `if` that is a condition to render
281
+ # @param options [Hash<Symbol, Proc>]
282
+ # @option options [Proc] if a condition to decide if this attribute should be serialized
191
283
  # @param block [Block] the block called during serialization
192
284
  # @raise [ArgumentError] if block is absent
285
+ # @return [void]
193
286
  def attribute(name, **options, &block)
194
287
  raise ArgumentError, 'No block given in attribute method' unless block
195
288
 
@@ -198,12 +291,14 @@ module Alba
198
291
 
199
292
  # Set One association
200
293
  #
201
- # @param name [String, Symbol]
202
- # @param condition [Proc]
203
- # @param resource [Class<Alba::Resource>]
204
- # @param key [String, Symbol] used as key when given
205
- # @param options [Hash] option hash including `if` that is a condition to render
294
+ # @param name [String, Symbol] name of the association, used as key when `key` param doesn't exist
295
+ # @param condition [Proc, nil] a Proc to modify the association
296
+ # @param resource [Class<Alba::Resource>, String, nil] representing resource for this association
297
+ # @param key [String, Symbol, nil] used as key when given
298
+ # @param options [Hash<Symbol, Proc>]
299
+ # @option options [Proc] if a condition to decide if this association should be serialized
206
300
  # @param block [Block]
301
+ # @return [void]
207
302
  # @see Alba::One#initialize
208
303
  def one(name, condition = nil, resource: nil, key: nil, **options, &block)
209
304
  nesting = self.name&.rpartition('::')&.first
@@ -214,12 +309,14 @@ module Alba
214
309
 
215
310
  # Set Many association
216
311
  #
217
- # @param name [String, Symbol]
218
- # @param condition [Proc]
219
- # @param resource [Class<Alba::Resource>]
220
- # @param key [String, Symbol] used as key when given
221
- # @param options [Hash] option hash including `if` that is a condition to render
312
+ # @param name [String, Symbol] name of the association, used as key when `key` param doesn't exist
313
+ # @param condition [Proc, nil] a Proc to filter the collection
314
+ # @param resource [Class<Alba::Resource>, String, nil] representing resource for this association
315
+ # @param key [String, Symbol, nil] used as key when given
316
+ # @param options [Hash<Symbol, Proc>]
317
+ # @option options [Proc] if a condition to decide if this association should be serialized
222
318
  # @param block [Block]
319
+ # @return [void]
223
320
  # @see Alba::Many#initialize
224
321
  def many(name, condition = nil, resource: nil, key: nil, **options, &block)
225
322
  nesting = self.name&.rpartition('::')&.first
@@ -231,14 +328,48 @@ module Alba
231
328
  # Set key
232
329
  #
233
330
  # @param key [String, Symbol]
331
+ # @deprecated Use {#root_key} instead
234
332
  def key(key)
333
+ Alba::Deprecation.warn '[DEPRECATION] `key` is deprecated, use `root_key` instead.'
235
334
  @_key = key.respond_to?(:to_sym) ? key.to_sym : key
236
335
  end
237
336
 
337
+ # Set root key
338
+ #
339
+ # @param key [String, Symbol]
340
+ # @param key_for_collection [String, Symbol]
341
+ # @raise [NoMethodError] when key doesn't respond to `to_sym` method
342
+ def root_key(key, key_for_collection = nil)
343
+ @_key = key.to_sym
344
+ @_key_for_collection = key_for_collection&.to_sym
345
+ end
346
+
238
347
  # Set key to true
239
348
  #
349
+ # @deprecated Use {#root_key!} instead
240
350
  def key!
351
+ Alba::Deprecation.warn '[DEPRECATION] `key!` is deprecated, use `root_key!` instead.'
352
+ @_key = true
353
+ @_key_for_collection = true
354
+ end
355
+
356
+ # Set root key to true
357
+ def root_key!
241
358
  @_key = true
359
+ @_key_for_collection = true
360
+ end
361
+
362
+ # Set metadata
363
+ def meta(&block)
364
+ @_meta = block
365
+ end
366
+
367
+ # Set layout
368
+ #
369
+ # @params file [String] name of the layout file
370
+ # @params inline [Proc] a proc returning JSON string or a Hash representing JSON
371
+ def layout(file: nil, inline: nil)
372
+ @_layout = file || inline
242
373
  end
243
374
 
244
375
  # Delete attributes
@@ -254,20 +385,32 @@ module Alba
254
385
  # Transform keys as specified type
255
386
  #
256
387
  # @param type [String, Symbol]
257
- def transform_keys(type)
258
- @_transform_keys = type.to_sym
388
+ # @param root [Boolean] decides if root key also should be transformed
389
+ def transform_keys(type, root: nil)
390
+ @_transform_key_function = KeyTransformFactory.create(type.to_sym)
391
+ @_transforming_root_key = root
259
392
  end
260
393
 
261
394
  # Set error handler
395
+ # If this is set it's used as a error handler overriding global one
262
396
  #
263
- # @param [Symbol] handler
264
- # @param [Block]
397
+ # @param handler [Symbol] `:raise`, `:ignore` or `:nullify`
398
+ # @param block [Block]
265
399
  def on_error(handler = nil, &block)
266
400
  raise ArgumentError, 'You cannot specify error handler with both Symbol and block' if handler && block
267
401
  raise ArgumentError, 'You must specify error handler with either Symbol or block' unless handler || block
268
402
 
269
403
  @_on_error = handler || block
270
404
  end
405
+
406
+ # Set nil handler
407
+ #
408
+ # @param block [Block]
409
+ def on_nil(&block)
410
+ @_on_nil = block
411
+ end
412
+
413
+ # rubocop:enable Metrics/ParameterLists
271
414
  end
272
415
  end
273
416
  end
@@ -0,0 +1,61 @@
1
+ module Alba
2
+ # Representing typed attributes to encapsulate logic about types
3
+ class TypedAttribute
4
+ # @param name [Symbol, String]
5
+ # @param type [Symbol, Class]
6
+ # @param converter [Proc]
7
+ def initialize(name:, type:, converter:)
8
+ @name = name
9
+ @type = type
10
+ @converter = case converter
11
+ when true then default_converter
12
+ when false, nil then null_converter
13
+ else converter
14
+ end
15
+ end
16
+
17
+ # @param object [Object] target to check and convert type with
18
+ # @return [String, Integer, Boolean] type-checked or type-converted object
19
+ def value(object)
20
+ value, result = check(object)
21
+ result ? value : @converter.call(value)
22
+ rescue TypeError
23
+ raise TypeError, "Attribute #{@name} is expected to be #{@type} but actually #{display_value_for(value)}."
24
+ end
25
+
26
+ private
27
+
28
+ def check(object)
29
+ value = object.public_send(@name)
30
+ type_correct = case @type
31
+ when :String, ->(klass) { klass == String } then value.is_a?(String)
32
+ when :Integer, ->(klass) { klass == Integer } then value.is_a?(Integer)
33
+ when :Boolean then [true, false].include?(value)
34
+ else
35
+ raise Alba::UnsupportedType, "Unknown type: #{@type}"
36
+ end
37
+ [value, type_correct]
38
+ end
39
+
40
+ def default_converter
41
+ case @type
42
+ when :String, ->(klass) { klass == String }
43
+ ->(object) { object.to_s }
44
+ when :Integer, ->(klass) { klass == Integer }
45
+ ->(object) { Integer(object) }
46
+ when :Boolean
47
+ ->(object) { !!object }
48
+ else
49
+ raise Alba::UnsupportedType, "Unknown type: #{@type}"
50
+ end
51
+ end
52
+
53
+ def null_converter
54
+ ->(_) { raise TypeError }
55
+ end
56
+
57
+ def display_value_for(value)
58
+ value.nil? ? 'nil' : value.class.name
59
+ end
60
+ end
61
+ end
data/lib/alba/version.rb CHANGED
@@ -1,3 +1,3 @@
1
1
  module Alba
2
- VERSION = '1.1.0'.freeze
2
+ VERSION = '1.5.0'.freeze
3
3
  end