alba 1.1.0 → 1.5.0

Sign up to get free protection for your applications and to get access to all the features.
data/README.md CHANGED
@@ -2,6 +2,7 @@
2
2
  [![CI](https://github.com/okuramasafumi/alba/actions/workflows/main.yml/badge.svg)](https://github.com/okuramasafumi/alba/actions/workflows/main.yml)
3
3
  [![codecov](https://codecov.io/gh/okuramasafumi/alba/branch/master/graph/badge.svg?token=3D3HEZ5OXT)](https://codecov.io/gh/okuramasafumi/alba)
4
4
  [![Maintainability](https://api.codeclimate.com/v1/badges/fdab4cc0de0b9addcfe8/maintainability)](https://codeclimate.com/github/okuramasafumi/alba/maintainability)
5
+ [![Inline docs](http://inch-ci.org/github/okuramasafumi/alba.svg?branch=main)](http://inch-ci.org/github/okuramasafumi/alba)
5
6
  ![GitHub code size in bytes](https://img.shields.io/github/languages/code-size/okuramasafumi/alba)
6
7
  ![GitHub](https://img.shields.io/github/license/okuramasafumi/alba)
7
8
 
@@ -59,18 +60,16 @@ You can find the documentation on [RubyDoc](https://rubydoc.info/github/okuramas
59
60
 
60
61
  ## Features
61
62
 
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
63
  * Conditional attributes and associations
68
64
  * Selectable backend
69
65
  * Key transformation
70
66
  * Root key inference
71
67
  * Error handling
68
+ * Nil handling
72
69
  * Resource name inflection based on association name
73
70
  * Circular associations control
71
+ * [Experimental] Types for validation and conversion
72
+ * Layout
74
73
  * No runtime dependencies
75
74
 
76
75
  ## Anti features
@@ -102,6 +101,16 @@ You can set a backend like this:
102
101
  Alba.backend = :oj
103
102
  ```
104
103
 
104
+ #### Encoder configuration
105
+
106
+ You can also set JSON encoder directly with a Proc.
107
+
108
+ ```ruby
109
+ Alba.encoder = ->(object) { JSON.generate(object) }
110
+ ```
111
+
112
+ You can consider setting a backend with Symbol as a shortcut to set encoder.
113
+
105
114
  #### Inference configuration
106
115
 
107
116
  You can enable inference feature using `enable_inference!` method.
@@ -122,7 +131,7 @@ Alba.on_error :ignore
122
131
 
123
132
  For the details, see [Error handling section](#error-handling)
124
133
 
125
- ### Simple serialization with key
134
+ ### Simple serialization with root key
126
135
 
127
136
  ```ruby
128
137
  class User
@@ -139,7 +148,7 @@ end
139
148
  class UserResource
140
149
  include Alba::Resource
141
150
 
142
- key :user
151
+ root_key :user
143
152
 
144
153
  attributes :id, :name
145
154
 
@@ -202,12 +211,41 @@ UserResource.new(user).serialize
202
211
  # => '{"id":1,"articles":[{"title":"Hello World!"},{"title":"Super nice"}]}'
203
212
  ```
204
213
 
214
+ You can define associations inline if you don't need a class for association.
215
+
216
+ ```ruby
217
+ class ArticleResource
218
+ include Alba::Resource
219
+
220
+ attributes :title
221
+ end
222
+
223
+ class UserResource
224
+ include Alba::Resource
225
+
226
+ attributes :id
227
+
228
+ many :articles, resource: ArticleResource
229
+ end
230
+
231
+ # This class works the same as `UserResource`
232
+ class AnotherUserResource
233
+ include Alba::Resource
234
+
235
+ attributes :id
236
+
237
+ many :articles do
238
+ attributes :title
239
+ end
240
+ end
241
+ ```
242
+
205
243
  ### Inline definition with `Alba.serialize`
206
244
 
207
245
  `Alba.serialize` method is a shortcut to define everything inline.
208
246
 
209
247
  ```ruby
210
- Alba.serialize(user, key: :foo) do
248
+ Alba.serialize(user, root_key: :foo) do
211
249
  attributes :id
212
250
  many :articles do
213
251
  attributes :title, :body
@@ -216,6 +254,13 @@ end
216
254
  # => '{"foo":{"id":1,"articles":[{"title":"Hello World!","body":"Hello World!!!"},{"title":"Super nice","body":"Really nice!"}]}}'
217
255
  ```
218
256
 
257
+ `Alba.serialize` can be used when you don't know what kind of object you serialize. For example:
258
+
259
+ ```ruby
260
+ Alba.serialize(something)
261
+ # => Same as `FooResource.new(something).serialize` when `something` is an instance of `Foo`.
262
+ ```
263
+
219
264
  Although this might be useful sometimes, it's generally recommended to define a class for Resource.
220
265
 
221
266
  ### Inheritance and Ignorance
@@ -239,20 +284,21 @@ class GenericFooResource
239
284
  attributes :id, :name, :body
240
285
  end
241
286
 
242
- class RestrictedFooResouce < GenericFooResource
287
+ class RestrictedFooResource < GenericFooResource
243
288
  ignoring :id, :body
244
289
  end
245
290
 
246
- RestrictedFooResouce.new(foo).serialize
291
+ RestrictedFooResource.new(foo).serialize
247
292
  # => '{"name":"my foo"}'
248
- end
249
293
  ```
250
294
 
251
- ### Attribute key transformation
295
+ ### Key transformation
252
296
 
253
- ** Note: You need to install `active_support` gem to use `transform_keys` DSL.
297
+ If you want to use `transform_keys` DSL and you already have `active_support` installed, key transformation will work out of the box, using `ActiveSupport::Inflector`. If `active_support` is not around, you have 2 possibilities:
298
+ * install it
299
+ * use a [custom inflector](#custom-inflector)
254
300
 
255
- With `active_support` installed, you can transform attribute keys.
301
+ With `transform_keys` DSL, you can transform attribute keys.
256
302
 
257
303
  ```ruby
258
304
  class User
@@ -278,8 +324,69 @@ UserResourceCamel.new(user).serialize
278
324
  # => '{"id":1,"firstName":"Masafumi","lastName":"Okura"}'
279
325
  ```
280
326
 
327
+ You can also transform root key when:
328
+
329
+ * `Alba.enable_inference!` is called
330
+ * `root_key!` is called in Resource class
331
+ * `root` option of `transform_keys` is set to true or `Alba.enable_root_key_transformation!` is called.
332
+
333
+ ```ruby
334
+ Alba.enable_inference!
335
+
336
+ class BankAccount
337
+ attr_reader :account_number
338
+
339
+ def initialize(account_number)
340
+ @account_number = account_number
341
+ end
342
+ end
343
+
344
+ class BankAccountResource
345
+ include Alba::Resource
346
+
347
+ root_key!
348
+
349
+ attributes :account_number
350
+ transform_keys :dash, root: true
351
+ end
352
+
353
+ bank_account = BankAccount.new(123_456_789)
354
+ BankAccountResource.new(bank_account).serialize
355
+ # => '{"bank-account":{"account-number":123456789}}'
356
+ ```
357
+
358
+ This behavior to transform root key will become default at version 2.
359
+
281
360
  Supported transformation types are :camel, :lower_camel and :dash.
282
361
 
362
+ #### Custom inflector
363
+
364
+ A custom inflector can be plugged in as follows...
365
+ ```ruby
366
+ Alba.inflector = MyCustomInflector
367
+ ```
368
+ ...and has to implement following interface (the parameter `key` is of type `String`):
369
+ ```ruby
370
+ module InflectorInterface
371
+ def camelize(key)
372
+ raise "Not implemented"
373
+ end
374
+
375
+ def camelize_lower(key)
376
+ raise "Not implemented"
377
+ end
378
+
379
+ def dasherize(key)
380
+ raise "Not implemented"
381
+ end
382
+ end
383
+
384
+ ```
385
+ For example you could use `Dry::Inflector`, which implements exactly the above interface. If you are developing a `Hanami`-Application `Dry::Inflector` is around. In this case the following would be sufficient:
386
+ ```ruby
387
+ Alba.inflector = Dry::Inflector.new
388
+ ```
389
+
283
390
  ### Filtering attributes
284
391
 
285
392
  You can filter attributes by overriding `Alba::Resource#converter` method, but it's a bit tricky.
@@ -343,6 +450,20 @@ user = User.new(1, nil, nil)
343
450
  UserResource.new(user).serialize # => '{"id":1}'
344
451
  ```
345
452
 
453
+ ### Default
454
+
455
+ Alba doesn't support default value for attributes, but it's easy to set a default value.
456
+
457
+ ```ruby
458
+ class FooResource
459
+ attribute :bar do |foo|
460
+ foo.bar || 'default bar'
461
+ end
462
+ end
463
+ ```
464
+
465
+ We believe this is clearer than using some (not implemented yet) DSL such as `default` because there are some conditions where default values should be applied (`nil`, `blank?`, `empty?` etc.)
466
+
346
467
  ### Inference
347
468
 
348
469
  After `Alba.enable_inference!` called, Alba tries to infer root key and association resource name.
@@ -451,12 +572,254 @@ Alba.on_error do |error, object, key, attribute, resource_class|
451
572
  end
452
573
  ```
453
574
 
575
+ ### Nil handling
576
+
577
+ Sometimes we want to convert `nil` to different values such as empty string. Alba provides a flexible way to handle `nil`.
578
+
579
+ ```ruby
580
+ class User
581
+ attr_reader :id, :name, :age
582
+
583
+ def initialize(id, name = nil, age = nil)
584
+ @id = id
585
+ @name = name
586
+ @age = age
587
+ end
588
+ end
589
+
590
+ class UserResource
591
+ include Alba::Resource
592
+
593
+ on_nil { '' }
594
+
595
+ root_key :user, :users
596
+
597
+ attributes :id, :name, :age
598
+ end
599
+
600
+ UserResource.new(User.new(1)).serialize
601
+ # => '{"user":{"id":1,"name":"","age":""}}'
602
+ ```
603
+
604
+ You can get various information via block parameters.
605
+
606
+ ```ruby
607
+ class UserResource
608
+ include Alba::Resource
609
+
610
+ on_nil do |object, key|
611
+ if key == age
612
+ 20
613
+ else
614
+ "User#{object.id}"
615
+ end
616
+ end
617
+
618
+ root_key :user, :users
619
+
620
+ attributes :id, :name, :age
621
+ end
622
+
623
+ UserResource.new(User.new(1)).serialize
624
+ # => '{"user":{"id":1,"name":"User1","age":20}}'
625
+ ```
626
+
627
+ You can also set global nil handler.
628
+
629
+ ```ruby
630
+ Alba.on_nil { 'default name' }
631
+
632
+ class Foo
633
+ attr_reader :name
634
+ def initialize(name)
635
+ @name = name
636
+ end
637
+ end
638
+
639
+ class FooResource
640
+ include Alba::Resource
641
+
642
+ key :foo
643
+
644
+ attributes :name
645
+ end
646
+
647
+ FooResource.new(Foo.new).serialize
648
+ # => '{"foo":{"name":"default name"}}'
649
+
650
+ class FooResource2
651
+ include Alba::Resource
652
+
653
+ key :foo
654
+
655
+ on_nil { '' } # This is applied instead of global handler
656
+
657
+ attributes :name
658
+ end
659
+
660
+ FooResource2.new(Foo.new).serialize
661
+ # => '{"foo":{"name":""}}'
662
+ ```
663
+
664
+ ### Metadata
665
+
666
+ You can set a metadata with `meta` DSL or `meta` option.
667
+
668
+ ```ruby
669
+ class UserResource
670
+ include Alba::Resource
671
+
672
+ root_key :user, :users
673
+
674
+ attributes :id, :name
675
+
676
+ meta do
677
+ if object.is_a?(Enumerable)
678
+ {size: object.size}
679
+ else
680
+ {foo: :bar}
681
+ end
682
+ end
683
+ end
684
+
685
+ user = User.new(1, 'Masafumi OKURA', 'masafumi@example.com')
686
+ UserResource.new([user]).serialize
687
+ # => '{"users":[{"id":1,"name":"Masafumi OKURA"}],"meta":{"size":1}}'
688
+
689
+ # You can merge metadata with `meta` option
690
+
691
+ UserResource.new([user]).serialize(meta: {foo: :bar})
692
+ # => '{"users":[{"id":1,"name":"Masafumi OKURA"}],"meta":{"size":1,"foo":"bar"}}'
693
+
694
+ # You can set metadata with `meta` option alone
695
+
696
+ class UserResourceWithoutMeta
697
+ include Alba::Resource
698
+
699
+ root_key :user, :users
700
+
701
+ attributes :id, :name
702
+ end
703
+
704
+ UserResource.new([user]).serialize(meta: {foo: :bar})
705
+ # => '{"users":[{"id":1,"name":"Masafumi OKURA"}],"meta":{"foo":"bar"}}'
706
+ ```
707
+
708
+ You can use `object` method to access the underlying object and `params` to access the params in `meta` block.
709
+
710
+ Note that setting root key is required when setting a metadata.
711
+
454
712
  ### Circular associations control
455
713
 
714
+ **Note that this feature works correctly since version 1.3. In previous versions it doesn't work as expected.**
715
+
456
716
  You can control circular associations with `within` option. `within` option is a nested Hash such as `{book: {authors: books}}`. In this example, Alba serializes a book's authors' books. This means you can reference `BookResource` from `AuthorResource` and vice versa. This is really powerful when you have a complex data structure and serialize certain parts of it.
457
717
 
458
718
  For more details, please refer to [test code](https://github.com/okuramasafumi/alba/blob/master/test/usecases/circular_association_test.rb)
459
719
 
720
+ ### Experimental support of types
721
+
722
+ You can validate and convert input with types.
723
+
724
+ ```ruby
725
+ class User
726
+ attr_reader :id, :name, :age, :bio, :admin, :created_at
727
+
728
+ def initialize(id, name, age, bio = '', admin = false) # rubocop:disable Style/OptionalBooleanParameter
729
+ @id = id
730
+ @name = name
731
+ @age = age
732
+ @admin = admin
733
+ @bio = bio
734
+ @created_at = Time.new(2020, 10, 10)
735
+ end
736
+ end
737
+
738
+ class UserResource
739
+ include Alba::Resource
740
+
741
+ attributes :name, id: [String, true], age: [Integer, true], bio: String, admin: [:Boolean, true], created_at: [String, ->(object) { object.strftime('%F') }]
742
+ end
743
+
744
+ user = User.new(1, 'Masafumi OKURA', '32', 'Ruby dev')
745
+ UserResource.new(user).serialize
746
+ # => '{"name":"Masafumi OKURA","id":"1","age":32,"bio":"Ruby dev","admin":false,"created_at":"2020-10-10"}'
747
+ ```
748
+
749
+ Notice that `id` and `created_at` are converted to String and `age` is converted to Integer.
750
+
751
+ If type is not correct and auto conversion is disabled (default), `TypeError` occurs.
752
+
753
+ ```ruby
754
+ user = User.new(1, 'Masafumi OKURA', '32', nil) # bio is nil and auto conversion is disabled for bio
755
+ UserResource.new(user).serialize
756
+ # => TypeError, 'Attribute bio is expected to be String but actually nil.'
757
+ ```
758
+
759
+ Note that this feature is experimental and interfaces are subject to change.
760
+
761
+ ### Layout
762
+
763
+ Sometimes we'd like to serialize JSON into a template. In other words, we need some structure OUTSIDE OF serialized JSON. IN HTML world, we call it a "layout".
764
+
765
+ Alba supports serializing JSON in a layout. You need a file for layout and then to specify file with `layout` method.
766
+
767
+ ```erb
768
+ {
769
+ "header": "my_header",
770
+ "body": <%= serialized_json %>
771
+ }
772
+ ```
773
+
774
+ ```ruby
775
+ class FooResource
776
+ include Alba::Resource
777
+ layout file: 'my_layout.json.erb'
778
+ end
779
+ ```
780
+
781
+ Note that layout files are treated as `json` and `erb` and evaluated in a context of the resource, meaning
782
+
783
+ * A layout file must be a valid JSON
784
+ * You must write `<%= serialized_json %>` in a layout to put serialized JSON string into a layout
785
+ * You can access `params` in a layout so that you can add virtually any objects to a layout
786
+ * When you access `params`, it's usually a Hash. You can use `encode` method in a layout to convert `params` Hash into a JSON with the backend you use
787
+ * You can also access `object`, the underlying object for the resource
788
+
789
+ In case you don't want to have a file for layout, Alba lets you define and apply layouts inline:
790
+
791
+ ```ruby
792
+ class FooResource
793
+ include Alba::Resource
794
+ layout inline: proc do
795
+ {
796
+ header: 'my header',
797
+ body: serializable_hash
798
+ }
799
+ end
800
+ end
801
+ ```
802
+
803
+ In the example above, we specify a Proc which returns a Hash as an inline layout. In the Proc we can use `serializable_hash` method to access a Hash right before serialization.
804
+
805
+ You can also use a Proc which returns String, not a Hash, for an inline layout.
806
+
807
+ ```ruby
808
+ class FooResource
809
+ include Alba::Resource
810
+ layout inline: proc do
811
+ %({
812
+ "header": "my header",
813
+ "body": #{serialized_json}
814
+ })
815
+ end
816
+ end
817
+ ```
818
+
819
+ It looks similar to file layout but you must use string interpolation for method calls since it's not an ERB.
820
+
821
+ Also note that we use percentage notation here to use double quotes. Using single quotes in inline string layout causes the error which might be resolved in other ways.
822
+
460
823
  ### Caching
461
824
 
462
825
  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/alba.gemspec CHANGED
@@ -7,14 +7,14 @@ Gem::Specification.new do |spec|
7
7
  spec.email = ['masafumi.o1988@gmail.com']
8
8
 
9
9
  spec.summary = 'Alba is the fastest JSON serializer for Ruby.'
10
- spec.description = "Alba is designed to be a simple, easy to use and fast alternative to existing JSON serializers. Its performance is better than almost all gems which do similar things. The internal is so simple that it's easy to hack and maintain."
10
+ spec.description = "Alba is the fastest JSON serializer for Ruby. It focuses on performance, flexibility and usability."
11
11
  spec.homepage = 'https://github.com/okuramasafumi/alba'
12
12
  spec.license = 'MIT'
13
13
  spec.required_ruby_version = Gem::Requirement.new('>= 2.5.0')
14
14
 
15
15
  spec.metadata['homepage_uri'] = spec.homepage
16
16
  spec.metadata['source_code_uri'] = 'https://github.com/okuramasafumi/alba'
17
- spec.metadata['changelog_uri'] = 'https://github.com/okuramasafumi/alba/blob/master/CHANGELOG.md'
17
+ spec.metadata['changelog_uri'] = 'https://github.com/okuramasafumi/alba/blob/main/CHANGELOG.md'
18
18
 
19
19
  # Specify which files should be added to the gem when it is released.
20
20
  # The `git ls-files -z` loads the files in the RubyGem that have been added into git.