alba 1.1.0 → 1.2.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.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 36890dfa4b9b73b60f1d6a09cf674de7ff7a6dd495ff4b74bbb3d048f9cf5a85
4
- data.tar.gz: 3e3d49619f646be866262e14e21e5fba11e2caafbc24064d57eb9c549cb64e4d
3
+ metadata.gz: bbb5260b5a6f3697f2e9c42ba198e3e17a1b0dc2aa0ea1bd2feb9ed4b18364a2
4
+ data.tar.gz: 794c146a493068865d7284de00bedda11c3b2fabd2ce7dccaa781e72181d957e
5
5
  SHA512:
6
- metadata.gz: fc7e025a035b41dadaab5300096cf920eb3557a4be900d31b514dfc66e8ae803b0fb63d77ec06174d72d70ad2e07da81c491502c8462ad306f2379720222fc65
7
- data.tar.gz: 741f5c1c69b2809aec51d2a2a139d4d8e00060c9aa174bf5fb68d5dbcec7996096aced5fb377d77fa147d7b6086a65191c8c764802c65bbd88d3806751ec0eb4
6
+ metadata.gz: 46d179154f3981879a4cda7f5ea0360d7eeb3481788d895acefcbb5df156fde10ee1b0eb78bd399ab247449ea85100aceaa2dcb757a54ba64cce9450d82d1825
7
+ data.tar.gz: a3ee5f28b7bbc626d4dcf92db32017eba85f60df42df02f07d464cdaa8c190f5c07914db5e9f7db21f73fd1356529add373ef2c79ac82038d22f2c5dd5ee273c
@@ -0,0 +1,26 @@
1
+ ---
2
+ name: Bug report
3
+ about: Create a report to help us improve
4
+ title: ''
5
+ labels: bug
6
+ assignees: ''
7
+
8
+ ---
9
+
10
+ ## Describe the bug
11
+ A clear and concise description of what the bug is.
12
+
13
+ ## To Reproduce
14
+ Steps to reproduce the behavior:
15
+
16
+ ## Expected behavior
17
+ A clear and concise description of what you expected to happen.
18
+
19
+ ## Actual behavior
20
+ A clear and concise description of what actually happened.
21
+
22
+ ## Environment
23
+ - Ruby version:
24
+
25
+ ## Additional context
26
+ Add any other context about the problem here.
@@ -0,0 +1,20 @@
1
+ ---
2
+ name: Feature request
3
+ about: Suggest an idea for this project
4
+ title: ''
5
+ labels: enhancement
6
+ assignees: ''
7
+
8
+ ---
9
+
10
+ ## Is your feature request related to a problem? Please describe.
11
+ A clear and concise description of what the problem is. Ex. I'm always frustrated when [...]
12
+
13
+ ## Describe the solution you'd like
14
+ A clear and concise description of what you want to happen.
15
+
16
+ ## Describe alternatives you've considered
17
+ A clear and concise description of any alternative solutions or features you've considered.
18
+
19
+ ## Additional context
20
+ Add any other context or screenshots about the feature request here.
@@ -0,0 +1,26 @@
1
+ version: 2
2
+ updates:
3
+ - package-ecosystem: bundler
4
+ directory: "/"
5
+ schedule:
6
+ interval: daily
7
+ time: "20:00"
8
+ open-pull-requests-limit: 10
9
+ ignore:
10
+ - dependency-name: rubocop
11
+ versions:
12
+ - 1.12.0
13
+ - 1.9.0
14
+ - dependency-name: rubocop-performance
15
+ versions:
16
+ - 1.10.0
17
+ - 1.10.2
18
+ - dependency-name: oj
19
+ versions:
20
+ - 3.11.3
21
+ - dependency-name: minitest
22
+ versions:
23
+ - 5.14.4
24
+ - dependency-name: activesupport
25
+ versions:
26
+ - 6.1.2
data/.rubocop.yml CHANGED
@@ -21,6 +21,15 @@ AllCops:
21
21
  Bundler/GemComment:
22
22
  Enabled: false
23
23
 
24
+ # We'd like to write something like:
25
+ # assert_equal(
26
+ # expected,
27
+ # actual
28
+ # )
29
+ Layout/RedundantLineBreak:
30
+ Exclude:
31
+ - 'test/**/*'
32
+
24
33
  Layout/SpaceInsideHashLiteralBraces:
25
34
  EnforcedStyle: no_space
26
35
 
data/.yardopts CHANGED
@@ -1,2 +1,4 @@
1
1
  --no-private
2
2
  --exclude lib/alba/version.rb
3
+ --embed-mixin ClassMethods
4
+ --embed-mixin InstanceMethods
data/CHANGELOG.md CHANGED
@@ -6,6 +6,16 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
6
6
 
7
7
  ## [Unreleased]
8
8
 
9
+ ## [1.2.0] 2021-05-09
10
+
11
+ - [Fix] multiple word key inference [6c18e73]
12
+ - https://github.com/okuramasafumi/alba/pull/120
13
+ - Thank you @alfonsojimenez !
14
+ - [Feat] Add `Alba.enable_root_key_transformation!` [f172839]
15
+ - https://github.com/okuramasafumi/alba/pull/121
16
+ - [Feat] Implement type validation and auto conversion [cbe00c7]
17
+ - https://github.com/okuramasafumi/alba/pull/122
18
+
9
19
  ## [1.1.0] - 2021-04-23
10
20
 
11
21
  - [Feat] Implement circular associations control [71e1543]
data/Gemfile CHANGED
@@ -8,7 +8,7 @@ gem 'ffaker', require: false # For testing
8
8
  gem 'minitest', '~> 5.14' # For test
9
9
  gem 'rake', '~> 13.0' # For test and automation
10
10
  gem 'rubocop', '>= 0.79.0', require: false # For lint
11
- gem 'rubocop-minitest', '~> 0.11.0', require: false # For lint
11
+ gem 'rubocop-minitest', '~> 0.12.0', require: false # For lint
12
12
  gem 'rubocop-performance', '~> 1.11.0', require: false # For lint
13
13
  gem 'rubocop-rake', '>= 0.5.1', require: false # For lint
14
14
  gem 'rubocop-sensible', '~> 0.3.0', require: false # For lint
data/README.md CHANGED
@@ -59,11 +59,6 @@ You can find the documentation on [RubyDoc](https://rubydoc.info/github/okuramas
59
59
 
60
60
  ## Features
61
61
 
62
- * Resource-based serialization
63
- * Arbitrary attribute definition
64
- * One and many association with the ability to define them inline
65
- * Adding condition and filter to association
66
- * Parameters can be injected and used in attributes and associations
67
62
  * Conditional attributes and associations
68
63
  * Selectable backend
69
64
  * Key transformation
@@ -71,6 +66,7 @@ You can find the documentation on [RubyDoc](https://rubydoc.info/github/okuramas
71
66
  * Error handling
72
67
  * Resource name inflection based on association name
73
68
  * Circular associations control
69
+ * Types for validation and conversion
74
70
  * No runtime dependencies
75
71
 
76
72
  ## Anti features
@@ -248,7 +244,7 @@ RestrictedFooResouce.new(foo).serialize
248
244
  end
249
245
  ```
250
246
 
251
- ### Attribute key transformation
247
+ ### Key transformation
252
248
 
253
249
  ** Note: You need to install `active_support` gem to use `transform_keys` DSL.
254
250
 
@@ -278,6 +274,39 @@ UserResourceCamel.new(user).serialize
278
274
  # => '{"id":1,"firstName":"Masafumi","lastName":"Okura"}'
279
275
  ```
280
276
 
277
+ You can also transform root key when:
278
+
279
+ * `Alba.enable_inference!` is called
280
+ * `key!` is called in Resource class
281
+ * `root` option of `transform_keys` is set to true or `Alba.enable_root_key_transformation!` is called.
282
+
283
+ ```ruby
284
+ Alba.enable_inference!
285
+
286
+ class BankAccount
287
+ attr_reader :account_number
288
+
289
+ def initialize(account_number)
290
+ @account_number = account_number
291
+ end
292
+ end
293
+
294
+ class BankAccountResource
295
+ include Alba::Resource
296
+
297
+ key!
298
+
299
+ attributes :account_number
300
+ transform_keys :dash, root: true
301
+ end
302
+
303
+ bank_account = BankAccount.new(123_456_789)
304
+ BankAccountResource.new(bank_account).serialize
305
+ # => '{"bank-account":{"account-number":123456789}}'
306
+ ```
307
+
308
+ This behavior to transform root key will become default at version 2.
309
+
281
310
  Supported transformation types are :camel, :lower_camel and :dash.
282
311
 
283
312
  ### Filtering attributes
@@ -457,6 +486,45 @@ You can control circular associations with `within` option. `within` option is a
457
486
 
458
487
  For more details, please refer to [test code](https://github.com/okuramasafumi/alba/blob/master/test/usecases/circular_association_test.rb)
459
488
 
489
+ ### Types
490
+
491
+ You can validate and convert input with types.
492
+
493
+ ```ruby
494
+ class User
495
+ attr_reader :id, :name, :age, :bio, :admin, :created_at
496
+
497
+ def initialize(id, name, age, bio = '', admin = false) # rubocop:disable Style/OptionalBooleanParameter
498
+ @id = id
499
+ @name = name
500
+ @age = age
501
+ @admin = admin
502
+ @bio = bio
503
+ @created_at = Time.new(2020, 10, 10)
504
+ end
505
+ end
506
+
507
+ class UserResource
508
+ include Alba::Resource
509
+
510
+ attributes :name, id: [String, true], age: [Integer, true], bio: String, admin: [:Boolean, true], created_at: [String, ->(object) { object.strftime('%F') }]
511
+ end
512
+
513
+ user = User.new(1, 'Masafumi OKURA', '32', 'Ruby dev')
514
+ UserResource.new(user).serialize
515
+ # => '{"name":"Masafumi OKURA","id":"1","age":32,"bio":"Ruby dev","admin":false,"created_at":"2020-10-10"}'
516
+ ```
517
+
518
+ Notice that `id` and `created_at` are converted to String and `age` is converted to Integer.
519
+
520
+ If type is not correct and auto conversion is disabled (default), `TypeError` occurs.
521
+
522
+ ```ruby
523
+ user = User.new(1, 'Masafumi OKURA', '32', nil) # bio is nil and auto conversion is disabled for bio
524
+ UserResource.new(user).serialize
525
+ # => TypeError, 'Attribute bio is expected to be String but actually nil.'
526
+ ```
527
+
460
528
  ### Caching
461
529
 
462
530
  Currently, Alba doesn't support caching, primarily due to the behavior of `ActiveRecord::Relation`'s cache. See [the issue](https://github.com/rails/rails/issues/41784).
data/SECURITY.md ADDED
@@ -0,0 +1,12 @@
1
+ # Security Policy
2
+
3
+ ## Supported Versions
4
+
5
+ | Version | Supported |
6
+ | ------- | ------------------ |
7
+ | 1.x.y | :white_check_mark: |
8
+ | < 1.0 | :x: |
9
+
10
+ ## Reporting a Vulnerability
11
+
12
+ If you find a vulnerability of Alba, please contact me (OKURA Masafumi) via [email](masafumi.o1988@gmail.com). I'll report back within a few days.
data/benchmark/local.rb CHANGED
@@ -17,6 +17,7 @@ gemfile(true) do
17
17
  gem "jbuilder"
18
18
  gem "jsonapi-serializer" # successor of fast_jsonapi
19
19
  gem "multi_json"
20
+ gem "primalize"
20
21
  gem "oj"
21
22
  gem "representable"
22
23
  gem "sqlite3"
@@ -213,6 +214,27 @@ class JsonApiSameFormatPostSerializer < JsonApiSameFormatSerializer
213
214
  end
214
215
  end
215
216
 
217
+ # --- Primalize serializers ---
218
+ #
219
+ class PrimalizeCommentResource < Primalize::Single
220
+ attributes id: integer, body: string
221
+ end
222
+
223
+ class PrimalizePostResource < Primalize::Single
224
+ alias post object
225
+
226
+ attributes(
227
+ id: integer,
228
+ body: string,
229
+ comments: array(primalize(PrimalizeCommentResource)),
230
+ commenter_names: array(string),
231
+ )
232
+
233
+ def commenter_names
234
+ post.commenters.pluck(:name)
235
+ end
236
+ end
237
+
216
238
  # --- Representable serializers ---
217
239
 
218
240
  require "representable"
@@ -265,6 +287,7 @@ blueprinter = Proc.new { PostBlueprint.render(post) }
265
287
  jbuilder = Proc.new { post.to_builder.target! }
266
288
  jsonapi = proc { JsonApiStandardPostSerializer.new(post).to_json }
267
289
  jsonapi_same_format = proc { JsonApiSameFormatPostSerializer.new(post).to_json }
290
+ primalize = proc { PrimalizePostResource.new(post).to_json }
268
291
  rails = Proc.new { ActiveSupport::JSON.encode(post.serializable_hash(include: :comments)) }
269
292
  representable = Proc.new { PostRepresenter.new(post).to_json }
270
293
 
@@ -279,6 +302,7 @@ puts "Serializer outputs ----------------------------------"
279
302
  jbuilder: jbuilder, # different order
280
303
  jsonapi: jsonapi, # nested JSON:API format
281
304
  jsonapi_same_format: jsonapi_same_format,
305
+ primalize: primalize,
282
306
  rails: rails,
283
307
  representable: representable
284
308
  }.each { |name, serializer| puts "#{name.to_s.ljust(24, ' ')} #{serializer.call}" }
@@ -295,6 +319,7 @@ Benchmark.bmbm do |x|
295
319
  x.report(:jbuilder) { time.times(&jbuilder) }
296
320
  x.report(:jsonapi) { time.times(&jsonapi) }
297
321
  x.report(:jsonapi_same_format) { time.times(&jsonapi_same_format) }
322
+ x.report(:primalize) { time.times(&primalize) }
298
323
  x.report(:rails) { time.times(&rails) }
299
324
  x.report(:representable) { time.times(&representable) }
300
325
  end
@@ -308,6 +333,7 @@ Benchmark.ips do |x|
308
333
  x.report(:jbuilder, &jbuilder)
309
334
  x.report(:jsonapi, &jsonapi)
310
335
  x.report(:jsonapi_same_format, &jsonapi_same_format)
336
+ x.report(:primalize, &primalize)
311
337
  x.report(:rails, &rails)
312
338
  x.report(:representable, &representable)
313
339
 
data/codecov.yml CHANGED
@@ -3,3 +3,6 @@ coverage:
3
3
  project:
4
4
  default:
5
5
  informational: true
6
+ patch:
7
+ default:
8
+ target: 90%
data/lib/alba.rb CHANGED
@@ -9,8 +9,11 @@ module Alba
9
9
  # Error class for backend which is not supported
10
10
  class UnsupportedBackend < Error; end
11
11
 
12
+ # Error class for type which is not supported
13
+ class UnsupportedType < Error; end
14
+
12
15
  class << self
13
- attr_reader :backend, :encoder, :inferring, :_on_error
16
+ attr_reader :backend, :encoder, :inferring, :_on_error, :transforming_root_key
14
17
 
15
18
  # Set the backend, which actually serializes object into JSON
16
19
  #
@@ -66,6 +69,16 @@ module Alba
66
69
  @_on_error = handler || block
67
70
  end
68
71
 
72
+ # Enable root key transformation
73
+ def enable_root_key_transformation!
74
+ @transforming_root_key = true
75
+ end
76
+
77
+ # Disable root key transformation
78
+ def disable_root_key_transformation!
79
+ @transforming_root_key = false
80
+ end
81
+
69
82
  private
70
83
 
71
84
  def set_encoder
@@ -109,4 +122,5 @@ module Alba
109
122
 
110
123
  @encoder = default_encoder
111
124
  @_on_error = :raise
125
+ @transforming_root_key = false # TODO: This will be true since 2.0
112
126
  end
data/lib/alba/resource.rb CHANGED
@@ -6,7 +6,7 @@ module Alba
6
6
  module Resource
7
7
  # @!parse include InstanceMethods
8
8
  # @!parse extend ClassMethods
9
- DSLS = {_attributes: {}, _key: nil, _transform_keys: nil, _on_error: nil}.freeze
9
+ DSLS = {_attributes: {}, _key: nil, _transform_keys: nil, _transforming_root_key: false, _on_error: nil}.freeze
10
10
  private_constant :DSLS
11
11
 
12
12
  # @private
@@ -58,13 +58,12 @@ module Alba
58
58
 
59
59
  # @return [String]
60
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
61
+ return @_key.to_s unless @_key == true && Alba.inferring
62
+
63
+ resource_name = self.class.name.demodulize.delete_suffix('Resource').underscore
64
+ key = collection? ? resource_name.pluralize : resource_name
65
+ transforming_root_key = @_transforming_root_key.nil? ? Alba.transforming_root_key : @_transforming_root_key
66
+ transforming_root_key ? transform_key(key) : key
68
67
  end
69
68
 
70
69
  def converter
@@ -76,7 +75,7 @@ module Alba
76
75
  else
77
76
  [key, fetch_attribute(object, attribute)]
78
77
  end
79
- rescue ::Alba::Error, FrozenError
78
+ rescue ::Alba::Error, FrozenError, TypeError
80
79
  raise
81
80
  rescue StandardError => e
82
81
  handle_error(e, object, key, attribute)
@@ -136,11 +135,55 @@ module Alba
136
135
  return unless within
137
136
 
138
137
  attribute.to_hash(object, params: params, within: within)
138
+ when Hash # Typed Attribute
139
+ typed_attribute(object, attribute)
139
140
  else
140
141
  raise ::Alba::Error, "Unsupported type of attribute: #{attribute.class}"
141
142
  end
142
143
  end
143
144
 
145
+ def typed_attribute(object, hash)
146
+ attr_name = hash[:attr_name]
147
+ type = hash[:type]
148
+ type_converter = hash[:type_converter]
149
+ value, result = type_check(object, attr_name, type)
150
+ return value if result
151
+ raise TypeError if !result && !type_converter
152
+
153
+ type_converter = type_converter_for(type) if type_converter == true
154
+ type_converter.call(value)
155
+ rescue TypeError
156
+ raise TypeError, "Attribute #{attr_name} is expected to be #{type} but actually #{value.nil? ? 'nil' : value.class.name}."
157
+ end
158
+
159
+ def type_check(object, attr_name, type)
160
+ value = object.public_send(attr_name)
161
+ type_correct = case type
162
+ when :String, ->(klass) { klass == String }
163
+ value.is_a?(String)
164
+ when :Integer, ->(klass) { klass == Integer }
165
+ value.is_a?(Integer)
166
+ when :Boolean
167
+ [true, false].include?(attr_name)
168
+ else
169
+ raise Alba::UnsupportedType, "Unknown type: #{type}"
170
+ end
171
+ [value, type_correct]
172
+ end
173
+
174
+ def type_converter_for(type)
175
+ case type
176
+ when :String, ->(klass) { klass == String }
177
+ ->(object) { object.to_s }
178
+ when :Integer, ->(klass) { klass == Integer }
179
+ ->(object) { Integer(object) }
180
+ when :Boolean
181
+ ->(object) { !!object }
182
+ else
183
+ raise Alba::UnsupportedType, "Unknown type: #{type}"
184
+ end
185
+ end
186
+
144
187
  def check_within
145
188
  case @within
146
189
  when Hash # Traverse within tree
@@ -177,11 +220,19 @@ module Alba
177
220
  #
178
221
  # @param attrs [Array<String, Symbol>]
179
222
  # @param options [Hash] option hash including `if` that is a condition to render these attributes
180
- def attributes(*attrs, **options)
223
+ def attributes(*attrs, if: nil, **attrs_with_types) # rubocop:disable Naming/MethodParameterName
224
+ if_value = binding.local_variable_get(:if)
181
225
  attrs.each do |attr_name|
182
- attr = options[:if] ? [attr_name.to_sym, options[:if]] : attr_name.to_sym
226
+ attr = if_value ? [attr_name.to_sym, if_value] : attr_name.to_sym
183
227
  @_attributes[attr_name.to_sym] = attr
184
228
  end
229
+ attrs_with_types.each do |attr_name, type_and_converter|
230
+ attr_name = attr_name.to_sym
231
+ type, type_converter = type_and_converter
232
+ typed_attr = {attr_name: attr_name, type: type, type_converter: type_converter}
233
+ attr = if_value ? [typed_attr, if_value] : typed_attr
234
+ @_attributes[attr_name] = attr
235
+ end
185
236
  end
186
237
 
187
238
  # Set an attribute with the given block
@@ -254,8 +305,10 @@ module Alba
254
305
  # Transform keys as specified type
255
306
  #
256
307
  # @param type [String, Symbol]
257
- def transform_keys(type)
308
+ # @param root [Boolean] decides if root key also should be transformed
309
+ def transform_keys(type, root: nil)
258
310
  @_transform_keys = type.to_sym
311
+ @_transforming_root_key = root
259
312
  end
260
313
 
261
314
  # Set error handler
data/lib/alba/version.rb CHANGED
@@ -1,3 +1,3 @@
1
1
  module Alba
2
- VERSION = '1.1.0'.freeze
2
+ VERSION = '1.2.0'.freeze
3
3
  end
data/sider.yml CHANGED
@@ -49,10 +49,8 @@ linter:
49
49
  # norc: true
50
50
 
51
51
  # # https://help.sider.review/getting-started/custom-configuration#ignore
52
- # ignore:
53
- # - "*.pdf"
54
- # - "*.mp4"
55
- # - "images/**"
52
+ ignore:
53
+ - 'test/**/*'
56
54
 
57
55
  # # https://help.sider.review/getting-started/custom-configuration#branchesexclude
58
56
  # branches:
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: 1.1.0
4
+ version: 1.2.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-04-23 00:00:00.000000000 Z
11
+ date: 2021-05-09 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,6 +19,9 @@ executables: []
19
19
  extensions: []
20
20
  extra_rdoc_files: []
21
21
  files:
22
+ - ".github/ISSUE_TEMPLATE/bug_report.md"
23
+ - ".github/ISSUE_TEMPLATE/feature_request.md"
24
+ - ".github/dependabot.yml"
22
25
  - ".github/workflows/main.yml"
23
26
  - ".gitignore"
24
27
  - ".rubocop.yml"
@@ -29,6 +32,7 @@ files:
29
32
  - LICENSE.txt
30
33
  - README.md
31
34
  - Rakefile
35
+ - SECURITY.md
32
36
  - alba.gemspec
33
37
  - benchmark/local.rb
34
38
  - bin/console