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.
- checksums.yaml +4 -4
- data/.github/ISSUE_TEMPLATE/bug_report.md +26 -0
- data/.github/ISSUE_TEMPLATE/feature_request.md +20 -0
- data/.github/dependabot.yml +26 -0
- data/.github/workflows/perf.yml +21 -0
- data/.rubocop.yml +18 -8
- data/.yardopts +2 -0
- data/CHANGELOG.md +40 -0
- data/Gemfile +4 -3
- data/README.md +377 -14
- data/SECURITY.md +12 -0
- data/alba.gemspec +2 -2
- data/benchmark/collection.rb +441 -0
- data/benchmark/{local.rb → single_resource.rb} +120 -15
- data/codecov.yml +3 -0
- data/docs/migrate_from_active_model_serializers.md +359 -0
- data/docs/migrate_from_jbuilder.md +223 -0
- data/gemfiles/all.gemfile +1 -1
- data/gemfiles/without_active_support.gemfile +1 -1
- data/gemfiles/without_oj.gemfile +1 -1
- data/lib/alba/association.rb +14 -17
- data/lib/alba/default_inflector.rb +36 -0
- data/lib/alba/deprecation.rb +14 -0
- data/lib/alba/key_transform_factory.rb +33 -0
- data/lib/alba/many.rb +1 -1
- data/lib/alba/resource.rb +226 -83
- data/lib/alba/typed_attribute.rb +61 -0
- data/lib/alba/version.rb +1 -1
- data/lib/alba.rb +82 -21
- data/script/perf_check.rb +174 -0
- data/sider.yml +2 -4
- metadata +20 -9
- data/lib/alba/key_transformer.rb +0 -32
data/README.md
CHANGED
@@ -2,6 +2,7 @@
|
|
2
2
|
[](https://github.com/okuramasafumi/alba/actions/workflows/main.yml)
|
3
3
|
[](https://codecov.io/gh/okuramasafumi/alba)
|
4
4
|
[](https://codeclimate.com/github/okuramasafumi/alba/maintainability)
|
5
|
+
[](http://inch-ci.org/github/okuramasafumi/alba)
|
5
6
|

|
6
7
|

|
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
|
-
|
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,
|
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
|
287
|
+
class RestrictedFooResource < GenericFooResource
|
243
288
|
ignoring :id, :body
|
244
289
|
end
|
245
290
|
|
246
|
-
|
291
|
+
RestrictedFooResource.new(foo).serialize
|
247
292
|
# => '{"name":"my foo"}'
|
248
|
-
end
|
249
293
|
```
|
250
294
|
|
251
|
-
###
|
295
|
+
### Key transformation
|
252
296
|
|
253
|
-
|
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 `
|
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
|
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/
|
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.
|