alba 0.12.0 → 1.1.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -2,25 +2,40 @@ module Alba
2
2
  # Base class for `One` and `Many`
3
3
  # Child class should implement `to_hash` method
4
4
  class Association
5
+ attr_reader :object
6
+
5
7
  # @param name [Symbol] name of the method to fetch association
6
8
  # @param condition [Proc] a proc filtering data
7
9
  # @param resource [Class<Alba::Resource>] a resource class for the association
8
10
  # @param block [Block] used to define resource when resource arg is absent
9
- def initialize(name:, condition: nil, resource: nil, &block)
11
+ def initialize(name:, condition: nil, resource: nil, nesting: nil, &block)
10
12
  @name = name
11
13
  @condition = condition
12
14
  @block = block
13
- @resource = resource || resource_class
14
- raise ArgumentError, 'resource or block is required' if @resource.nil? && @block.nil?
15
- end
15
+ @resource = resource
16
+ return if @resource
16
17
 
17
- # @abstract
18
- def to_hash
19
- :not_implemented
18
+ if @block
19
+ @resource = resource_class
20
+ elsif Alba.inferring
21
+ const_parent = nesting.nil? ? Object : Object.const_get(nesting)
22
+ @resource = const_parent.const_get("#{ActiveSupport::Inflector.classify(@name)}Resource")
23
+ else
24
+ raise ArgumentError, 'When Alba.inferring is false, either resource or block is required'
25
+ end
20
26
  end
21
27
 
22
28
  private
23
29
 
30
+ def constantize(resource)
31
+ case resource # rubocop:disable Style/MissingElse
32
+ when Class
33
+ resource
34
+ when Symbol, String
35
+ Object.const_get(resource)
36
+ end
37
+ end
38
+
24
39
  def resource_class
25
40
  klass = Class.new
26
41
  klass.include(Alba::Resource)
@@ -0,0 +1,32 @@
1
+ module Alba
2
+ # Transform keys using `ActiveSupport::Inflector`
3
+ module KeyTransformer
4
+ begin
5
+ require 'active_support/inflector'
6
+ rescue LoadError
7
+ raise ::Alba::Error, 'To use transform_keys, please install `ActiveSupport` gem.'
8
+ end
9
+
10
+ module_function
11
+
12
+ # Transform key as given transform_type
13
+ #
14
+ # @params key [String] key to be transformed
15
+ # @params transform_type [Symbol] transform type
16
+ # @return [String] transformed key
17
+ # @raise [Alba::Error] when transform_type is not supported
18
+ def transform(key, transform_type)
19
+ key = key.to_s
20
+ case transform_type
21
+ when :camel
22
+ ActiveSupport::Inflector.camelize(key)
23
+ when :lower_camel
24
+ ActiveSupport::Inflector.camelize(key, false)
25
+ when :dash
26
+ ActiveSupport::Inflector.dasherize(key)
27
+ else
28
+ raise ::Alba::Error, "Unknown transform_type: #{transform_type}. Supported transform_type are :camel, :lower_camel and :dash."
29
+ end
30
+ end
31
+ end
32
+ end
data/lib/alba/many.rb CHANGED
@@ -6,12 +6,16 @@ module Alba
6
6
  # Recursively converts objects into an Array of Hashes
7
7
  #
8
8
  # @param target [Object] the object having an association method
9
+ # @param within [Hash] determines what associations to be serialized. If not set, it serializes all associations.
9
10
  # @param params [Hash] user-given Hash for arbitrary data
10
11
  # @return [Array<Hash>]
11
- def to_hash(target, params: {})
12
- objects = target.public_send(@name)
13
- objects = @condition.call(objects, params) if @condition
14
- objects.map { |o| @resource.new(o, params: params).to_hash }
12
+ def to_hash(target, within: nil, params: {})
13
+ @object = target.public_send(@name)
14
+ @object = @condition.call(@object, params) if @condition
15
+ return if @object.nil?
16
+
17
+ @resource = constantize(@resource)
18
+ @object.map { |o| @resource.new(o, params: params, within: within).to_hash }
15
19
  end
16
20
  end
17
21
  end
data/lib/alba/one.rb CHANGED
@@ -6,12 +6,16 @@ module Alba
6
6
  # Recursively converts an object into a Hash
7
7
  #
8
8
  # @param target [Object] the object having an association method
9
+ # @param within [Hash] determines what associations to be serialized. If not set, it serializes all associations.
9
10
  # @param params [Hash] user-given Hash for arbitrary data
10
11
  # @return [Hash]
11
- def to_hash(target, params: {})
12
- object = target.public_send(@name)
13
- object = @condition.call(object, params) if @condition
14
- @resource.new(object, params: params).to_hash
12
+ def to_hash(target, within: nil, params: {})
13
+ @object = target.public_send(@name)
14
+ @object = @condition.call(object, params) if @condition
15
+ return if @object.nil?
16
+
17
+ @resource = constantize(@resource)
18
+ @resource.new(object, params: params, within: within).to_hash
15
19
  end
16
20
  end
17
21
  end
data/lib/alba/resource.rb CHANGED
@@ -1,4 +1,3 @@
1
- require_relative 'serializer'
2
1
  require_relative 'one'
3
2
  require_relative 'many'
4
3
 
@@ -7,7 +6,7 @@ module Alba
7
6
  module Resource
8
7
  # @!parse include InstanceMethods
9
8
  # @!parse extend ClassMethods
10
- DSLS = {_attributes: {}, _serializer: nil, _key: nil}.freeze
9
+ DSLS = {_attributes: {}, _key: nil, _transform_keys: nil, _on_error: nil}.freeze
11
10
  private_constant :DSLS
12
11
 
13
12
  # @private
@@ -25,32 +24,26 @@ module Alba
25
24
 
26
25
  # Instance methods
27
26
  module InstanceMethods
28
- attr_reader :object, :_key, :params
27
+ attr_reader :object, :params
29
28
 
30
29
  # @param object [Object] the object to be serialized
31
30
  # @param params [Hash] user-given Hash for arbitrary data
32
- def initialize(object, params: {})
31
+ # @param within [Hash] determines what associations to be serialized. If not set, it serializes all associations.
32
+ def initialize(object, params: {}, within: true)
33
33
  @object = object
34
34
  @params = params.freeze
35
+ @within = within
35
36
  DSLS.each_key { |name| instance_variable_set("@#{name}", self.class.public_send(name)) }
36
37
  end
37
38
 
38
- # Get serializer with `with` argument and serialize self with it
39
+ # Serialize object into JSON string
39
40
  #
40
- # @param with [nil, Proc, Alba::Serializer] selializer
41
+ # @param key [Symbol]
41
42
  # @return [String] serialized JSON string
42
- def serialize(with: nil)
43
- serializer = case with
44
- when nil
45
- @_serializer || empty_serializer
46
- when ->(obj) { obj.is_a?(Class) && obj <= Alba::Serializer }
47
- with
48
- when Proc
49
- inline_extended_serializer(with)
50
- else
51
- raise ArgumentError, 'Unexpected type for with, possible types are Class or Proc'
52
- end
53
- serializer.new(self).serialize
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)
54
47
  end
55
48
 
56
49
  # A Hash for serialization
@@ -61,40 +54,108 @@ module Alba
61
54
  end
62
55
  alias to_hash serializable_hash
63
56
 
64
- # @return [Symbol]
65
- def key
66
- @_key || self.class.name.delete_suffix('Resource').downcase.gsub(/:{2}/, '_').to_sym
67
- end
68
-
69
57
  private
70
58
 
59
+ # @return [String]
60
+ 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
68
+ end
69
+
71
70
  def converter
72
- lambda do |resource|
73
- @_attributes.transform_values do |attribute|
74
- case attribute
75
- when Symbol
76
- resource.public_send attribute
77
- when Proc
78
- instance_exec(resource, &attribute)
79
- when Alba::One, Alba::Many
80
- attribute.to_hash(resource, params: params)
71
+ lambda do |object|
72
+ arrays = @_attributes.map do |key, attribute|
73
+ key = transform_key(key)
74
+ if attribute.is_a?(Array) # Conditional
75
+ conditional_attribute(object, key, attribute)
81
76
  else
82
- raise ::Alba::Error, "Unsupported type of attribute: #{attribute.class}"
77
+ [key, fetch_attribute(object, attribute)]
83
78
  end
79
+ rescue ::Alba::Error, FrozenError
80
+ raise
81
+ rescue StandardError => e
82
+ handle_error(e, object, key, attribute)
84
83
  end
84
+ arrays.reject(&:empty?).to_h
85
+ end
86
+ end
87
+
88
+ def conditional_attribute(object, key, attribute)
89
+ condition = attribute.last
90
+ arity = condition.arity
91
+ return [] if arity <= 1 && !condition.call(object)
92
+
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)
100
+
101
+ [key, fetched_attribute]
102
+ end
103
+
104
+ def handle_error(error, object, key, attribute)
105
+ on_error = @_on_error || Alba._on_error
106
+ 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)
115
+ else
116
+ raise ::Alba::Error, "Unknown on_error: #{on_error.inspect}"
85
117
  end
86
118
  end
87
119
 
88
- def empty_serializer
89
- klass = Class.new
90
- klass.include Alba::Serializer
91
- klass
120
+ # Override this method to supply custom key transform method
121
+ def transform_key(key)
122
+ return key unless @_transform_keys
123
+
124
+ require_relative 'key_transformer'
125
+ KeyTransformer.transform(key, @_transform_keys)
126
+ end
127
+
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
137
+
138
+ attribute.to_hash(object, params: params, within: within)
139
+ else
140
+ raise ::Alba::Error, "Unsupported type of attribute: #{attribute.class}"
141
+ end
92
142
  end
93
143
 
94
- def inline_extended_serializer(with)
95
- klass = empty_serializer
96
- klass.class_eval(&with)
97
- klass
144
+ def check_within
145
+ 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
156
+ else
157
+ raise Alba::Error, "Unknown type for within option: #{@within.class}"
158
+ end
98
159
  end
99
160
 
100
161
  def collection?
@@ -115,19 +176,24 @@ module Alba
115
176
  # Set multiple attributes at once
116
177
  #
117
178
  # @param attrs [Array<String, Symbol>]
118
- def attributes(*attrs)
119
- attrs.each { |attr_name| @_attributes[attr_name.to_sym] = attr_name.to_sym }
179
+ # @param options [Hash] option hash including `if` that is a condition to render these attributes
180
+ def attributes(*attrs, **options)
181
+ attrs.each do |attr_name|
182
+ attr = options[:if] ? [attr_name.to_sym, options[:if]] : attr_name.to_sym
183
+ @_attributes[attr_name.to_sym] = attr
184
+ end
120
185
  end
121
186
 
122
187
  # Set an attribute with the given block
123
188
  #
124
189
  # @param name [String, Symbol] key name
190
+ # @param options [Hash] option hash including `if` that is a condition to render
125
191
  # @param block [Block] the block called during serialization
126
192
  # @raise [ArgumentError] if block is absent
127
- def attribute(name, &block)
193
+ def attribute(name, **options, &block)
128
194
  raise ArgumentError, 'No block given in attribute method' unless block
129
195
 
130
- @_attributes[name.to_sym] = block
196
+ @_attributes[name.to_sym] = options[:if] ? [block, options[:if]] : block
131
197
  end
132
198
 
133
199
  # Set One association
@@ -136,10 +202,13 @@ module Alba
136
202
  # @param condition [Proc]
137
203
  # @param resource [Class<Alba::Resource>]
138
204
  # @param key [String, Symbol] used as key when given
205
+ # @param options [Hash] option hash including `if` that is a condition to render
139
206
  # @param block [Block]
140
207
  # @see Alba::One#initialize
141
- def one(name, condition = nil, resource: nil, key: nil, &block)
142
- @_attributes[key&.to_sym || name.to_sym] = One.new(name: name, condition: condition, resource: resource, &block)
208
+ def one(name, condition = nil, resource: nil, key: nil, **options, &block)
209
+ nesting = self.name&.rpartition('::')&.first
210
+ one = One.new(name: name, condition: condition, resource: resource, nesting: nesting, &block)
211
+ @_attributes[key&.to_sym || name.to_sym] = options[:if] ? [one, options[:if]] : one
143
212
  end
144
213
  alias has_one one
145
214
 
@@ -149,25 +218,27 @@ module Alba
149
218
  # @param condition [Proc]
150
219
  # @param resource [Class<Alba::Resource>]
151
220
  # @param key [String, Symbol] used as key when given
221
+ # @param options [Hash] option hash including `if` that is a condition to render
152
222
  # @param block [Block]
153
223
  # @see Alba::Many#initialize
154
- def many(name, condition = nil, resource: nil, key: nil, &block)
155
- @_attributes[key&.to_sym || name.to_sym] = Many.new(name: name, condition: condition, resource: resource, &block)
224
+ def many(name, condition = nil, resource: nil, key: nil, **options, &block)
225
+ nesting = self.name&.rpartition('::')&.first
226
+ many = Many.new(name: name, condition: condition, resource: resource, nesting: nesting, &block)
227
+ @_attributes[key&.to_sym || name.to_sym] = options[:if] ? [many, options[:if]] : many
156
228
  end
157
229
  alias has_many many
158
230
 
159
- # Set serializer for the resource
160
- #
161
- # @param name [Alba::Serializer]
162
- def serializer(name)
163
- @_serializer = name <= Alba::Serializer ? name : nil
164
- end
165
-
166
231
  # Set key
167
232
  #
168
233
  # @param key [String, Symbol]
169
234
  def key(key)
170
- @_key = key.to_sym
235
+ @_key = key.respond_to?(:to_sym) ? key.to_sym : key
236
+ end
237
+
238
+ # Set key to true
239
+ #
240
+ def key!
241
+ @_key = true
171
242
  end
172
243
 
173
244
  # Delete attributes
@@ -179,6 +250,24 @@ module Alba
179
250
  @_attributes.delete(attr_name.to_sym)
180
251
  end
181
252
  end
253
+
254
+ # Transform keys as specified type
255
+ #
256
+ # @param type [String, Symbol]
257
+ def transform_keys(type)
258
+ @_transform_keys = type.to_sym
259
+ end
260
+
261
+ # Set error handler
262
+ #
263
+ # @param [Symbol] handler
264
+ # @param [Block]
265
+ def on_error(handler = nil, &block)
266
+ raise ArgumentError, 'You cannot specify error handler with both Symbol and block' if handler && block
267
+ raise ArgumentError, 'You must specify error handler with either Symbol or block' unless handler || block
268
+
269
+ @_on_error = handler || block
270
+ end
182
271
  end
183
272
  end
184
273
  end
data/lib/alba/version.rb CHANGED
@@ -1,3 +1,3 @@
1
1
  module Alba
2
- VERSION = '0.12.0'.freeze
2
+ VERSION = '1.1.0'.freeze
3
3
  end
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: alba
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.12.0
4
+ version: 1.1.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - OKURA Masafumi
8
8
  autorequire:
9
9
  bindir: exe
10
10
  cert_chain: []
11
- date: 2021-01-20 00:00:00.000000000 Z
11
+ date: 2021-04-23 00:00:00.000000000 Z
12
12
  dependencies: []
13
13
  description: Alba is designed to be a simple, easy to use and fast alternative to
14
14
  existing JSON serializers. Its performance is better than almost all gems which
@@ -19,25 +19,30 @@ executables: []
19
19
  extensions: []
20
20
  extra_rdoc_files: []
21
21
  files:
22
+ - ".github/workflows/main.yml"
22
23
  - ".gitignore"
23
24
  - ".rubocop.yml"
24
- - ".travis.yml"
25
25
  - ".yardopts"
26
+ - CHANGELOG.md
26
27
  - CODE_OF_CONDUCT.md
27
28
  - Gemfile
28
- - Gemfile.lock
29
29
  - LICENSE.txt
30
30
  - README.md
31
31
  - Rakefile
32
32
  - alba.gemspec
33
+ - benchmark/local.rb
33
34
  - bin/console
34
35
  - bin/setup
36
+ - codecov.yml
37
+ - gemfiles/all.gemfile
38
+ - gemfiles/without_active_support.gemfile
39
+ - gemfiles/without_oj.gemfile
35
40
  - lib/alba.rb
36
41
  - lib/alba/association.rb
42
+ - lib/alba/key_transformer.rb
37
43
  - lib/alba/many.rb
38
44
  - lib/alba/one.rb
39
45
  - lib/alba/resource.rb
40
- - lib/alba/serializer.rb
41
46
  - lib/alba/version.rb
42
47
  - sider.yml
43
48
  homepage: https://github.com/okuramasafumi/alba
@@ -46,7 +51,7 @@ licenses:
46
51
  metadata:
47
52
  homepage_uri: https://github.com/okuramasafumi/alba
48
53
  source_code_uri: https://github.com/okuramasafumi/alba
49
- changelog_uri: https://github.com/okuramasafumi/alba/CHANGELOG.md
54
+ changelog_uri: https://github.com/okuramasafumi/alba/blob/master/CHANGELOG.md
50
55
  post_install_message:
51
56
  rdoc_options: []
52
57
  require_paths:
@@ -55,14 +60,14 @@ required_ruby_version: !ruby/object:Gem::Requirement
55
60
  requirements:
56
61
  - - ">="
57
62
  - !ruby/object:Gem::Version
58
- version: 2.5.7
63
+ version: 2.5.0
59
64
  required_rubygems_version: !ruby/object:Gem::Requirement
60
65
  requirements:
61
66
  - - ">="
62
67
  - !ruby/object:Gem::Version
63
68
  version: '0'
64
69
  requirements: []
65
- rubygems_version: 3.0.3
70
+ rubygems_version: 3.2.16
66
71
  signing_key:
67
72
  specification_version: 4
68
73
  summary: Alba is the fastest JSON serializer for Ruby.