alba 1.1.0 → 1.5.0

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.
@@ -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