README.md in alba-1.6.0 vs README.md in alba-2.0.0
- old
+ new
@@ -1,39 +1,45 @@
+![alba card](https://raw.githubusercontent.com/okuramasafumi/alba/main/logo/alba-card.png)
+----------
[![Gem Version](https://badge.fury.io/rb/alba.svg)](https://badge.fury.io/rb/alba)
[![CI](https://github.com/okuramasafumi/alba/actions/workflows/main.yml/badge.svg)](https://github.com/okuramasafumi/alba/actions/workflows/main.yml)
-[![codecov](https://codecov.io/gh/okuramasafumi/alba/branch/master/graph/badge.svg?token=3D3HEZ5OXT)](https://codecov.io/gh/okuramasafumi/alba)
+[![codecov](https://codecov.io/gh/okuramasafumi/alba/branch/main/graph/badge.svg?token=3D3HEZ5OXT)](https://codecov.io/gh/okuramasafumi/alba)
[![Maintainability](https://api.codeclimate.com/v1/badges/fdab4cc0de0b9addcfe8/maintainability)](https://codeclimate.com/github/okuramasafumi/alba/maintainability)
![GitHub code size in bytes](https://img.shields.io/github/languages/code-size/okuramasafumi/alba)
![GitHub](https://img.shields.io/github/license/okuramasafumi/alba)
# Alba
-Alba is the fastest JSON serializer for Ruby, JRuby, and TruffleRuby.
+Alba is a JSON serializer for Ruby, JRuby, and TruffleRuby.
## Discussions
Alba uses [GitHub Discussions](https://github.com/okuramasafumi/alba/discussions) to openly discuss the project.
If you've already used Alba, please consider posting your thoughts and feelings on [Feedback](https://github.com/okuramasafumi/alba/discussions/categories/feedback). The fact that you enjoy using Alba gives me energy to keep developing Alba!
If you have feature requests or interesting ideas, join us with [Ideas](https://github.com/okuramasafumi/alba/discussions/categories/ideas). Let's make Alba even better, together!
+## Resources
+
+If you want to know more about Alba, there's a [screencast](https://hanamimastery.com/episodes/21-serialization-with-alba) created by Sebastian from [Hanami Mastery](https://hanamimastery.com/). It covers basic features of Alba and how to use it in Hanami.
+
## Why Alba?
-Because it's fast, easy-to-use and extensible!
+Because it's fast, easy and feature rich!
### Fast
-Alba is faster than most of the alternatives. We have a [benchmark](https://github.com/okuramasafumi/alba/tree/master/benchmark).
+Alba is faster than most of the alternatives. We have a [benchmark](https://github.com/okuramasafumi/alba/tree/main/benchmark).
-### Easy to use
+### Easy
-Alba provides four DSLs, `attributes` to fetch attribute with its name, `attribute` to execute block for the attribute, `one` to seriaize single attribute with another resource, and `many` to serialize collection attribute with another resource. When you want to do something complex, there are many examples in this README so you can mimic them to get started.
+Alba is easy to use because there are only a few methods to remember. It's also easy to understand due to clean and short codebase. Finally it's easy to extend since it provides some methods for override to change default behavior of Alba.
-### Extensible
+### Feature rich
-Alba embraces extensibility through common techniques such as class inheritance and module inclusion. Alba provides its capacity with one module so you can still have your own class hierarchy.
+While Alba's core is simple, it provides additional features when you need them, For example, Alba provides [a way to control circular associations](#circular-associations-control), [inferring resource classes, root key and associations](#inference) and [supports layouts](#layout).
## Installation
Add this line to your application's Gemfile:
@@ -79,13 +85,17 @@
#### Backend configuration
Backend is the actual part serializing an object into JSON. Alba supports these backends.
-* Oj, the fastest. Gem installation required.
-* active_support, mostly for Rails. Gem installation required.
-* default or json, with no external dependencies.
+|name|description|requires_external_gem|
+|--|--|--|
+|`oj`, `oj_strict`|Using Oj in `strict` mode|Yes(C extension)|
+|`oj_rails`|It's `oj` but in `rails` mode|Yes(C extension)|
+|`oj_default`|It's `oj` but respects mode set by users|Yes(C extension)|
+|`active_support`|For Rails compatibility|Yes|
+|`default`, `json`|Using `json` gem|No|
You can set a backend like this:
```ruby
Alba.backend = :oj
@@ -175,12 +185,41 @@
user2 = User.new(2, 'Test User', 'test@example.com')
UserResource.new([user1, user2]).serialize
# => "{\"users\":[{\"id\":1,\"name\":\"Masafumi OKURA\",\"name_with_email\":\"Masafumi OKURA: masafumi@example.com\"},{\"id\":2,\"name\":\"Test User\",\"name_with_email\":\"Test User: test@example.com\"}]}"
```
+If you have a simple case where you want to change only the name, you can use the Symbol to Proc shortcut:
+
+```ruby
+class UserResource
+ include Alba::Resource
+
+ attribute :some_other_name, &:name
+end
+```
+
+#### Params
+
+You can pass a Hash to the resource for internal use. It can be used as "flags" to control attribute content.
+
+```ruby
+class UserResource
+ include Alba::Resource
+ attribute :name do |user|
+ params[:upcase] ? user.name.upcase : user.name
+ end
+end
+
+user = User.new(1, 'Masa', 'test@example.com')
+UserResource.new(user).serialize # => "{\"name\":\"foo\"}"
+UserResource.new(user, params: {upcase: true}).serialize # => "{\"name\":\"FOO\"}"
+```
+
### Serialization with associations
+Associations can be defined using the `association` macro, which is also aliased as `one`, `many`, `has_one`, and `has_many` for convenience.
+
```ruby
class User
attr_reader :id, :created_at, :updated_at
attr_accessor :articles
@@ -253,21 +292,22 @@
attributes :title
end
end
```
-You can "filter" association using second proc argument. This proc takes association object and `params`.
+You can "filter" association using second proc argument. This proc takes association object, `params` and initial object.
This feature is useful when you want to modify association, such as adding `includes` or `order` to ActiveRecord relations.
```ruby
class User
- attr_reader :id
+ attr_reader :id, :banned
attr_accessor :articles
- def initialize(id)
+ def initialize(id, banned = false)
@id = id
+ @banned = banned
@articles = []
end
end
class Article
@@ -291,13 +331,13 @@
attributes :id
# Second proc works as a filter
many :articles,
- proc { |articles, params|
+ proc { |articles, params, user|
filter = params[:filter] || :odd?
- articles.select {|a| a.id.send(filter) }
+ articles.select {|a| a.id.send(filter) && !user.banned }
},
resource: ArticleResource
end
user = User.new(1)
@@ -310,10 +350,154 @@
# => '{"id":1,"articles":[{"title":"Hello World!"}]}'
UserResource.new(user, params: {filter: :even?}).serialize
# => '{"id":1,"articles":[{"title":"Super nice"}]}'
```
+You can change a key for association with `key` option.
+
+```ruby
+class UserResource
+ include Alba::Resource
+
+ attributes :id
+
+ many :articles,
+ key: 'my_articles', # Set key here
+ resource: ArticleResource
+end
+UserResource.new(user).serialize
+# => '{"id":1,"my_articles":[{"title":"Hello World!"}]}'
+```
+
+You can omit resource option if you enable Alba's inference feature.
+
+```ruby
+class UserResource
+ include Alba::Resource
+
+ attributes :id
+
+ many :articles # Using `ArticleResource`
+end
+UserResource.new(user).serialize
+# => '{"id":1,"my_articles":[{"title":"Hello World!"}]}'
+```
+
+If you need complex logic to determine what resource to use for association, you can use a Proc for resource option.
+
+```ruby
+class UserResource
+ include Alba::Resource
+
+ attributes :id
+
+ many :articles, ->(article) { article.with_comment? ? ArticleWithCommentResource : ArticleResource }
+end
+```
+
+Note that using a Proc slows down serialization if there are too `many` associated objects.
+
+#### Params override
+
+Associations can override params. This is useful when associations are deeply nested.
+
+```ruby
+class BazResource
+ include Alba::Resource
+
+ attributes :data
+ attributes :secret, if: proc { params[:expose_secret] }
+end
+
+class BarResource
+ include Alba::Resource
+
+ one :baz, resource: BazResource
+end
+
+class FooResource
+ include Alba::Resource
+
+ root_key :foo
+
+ one :bar, resource: BarResource
+end
+
+class FooResourceWithParamsOverride
+ include Alba::Resource
+
+ root_key :foo
+
+ one :bar, resource: BarResource, params: { expose_secret: false }
+end
+
+Baz = Struct.new(:data, :secret)
+Bar = Struct.new(:baz)
+Foo = Struct.new(:bar)
+
+foo = Foo.new(Bar.new(Baz.new(1, 'secret')))
+FooResource.new(foo, params: {expose_secret: true}).serialize # => '{"foo":{"bar":{"baz":{"data":1,"secret":"secret"}}}}'
+FooResourceWithParamsOverride.new(foo, params: {expose_secret: true}).serialize # => '{"foo":{"bar":{"baz":{"data":1}}}}'
+```
+
+### Nested Attribute
+
+Alba supports nested attributes that makes it easy to build complex data structure from single object.
+
+In order to define nested attributes, you can use `nested` or `nested_attribute` (alias of `nested`).
+
+```ruby
+class User
+ attr_accessor :id, :name, :email, :city, :zipcode
+
+ def initialize(id, name, email, city, zipcode)
+ @id = id
+ @name = name
+ @email = email
+ @city = city
+ @zipcode = zipcode
+ end
+end
+
+class UserResource
+ include Alba::Resource
+
+ root_key :user
+
+ attributes :id
+
+ nested_attribute :address do
+ attributes :city, :zipcode
+ end
+end
+
+user = User.new(1, 'Masafumi OKURA', 'masafumi@example.com', 'Tokyo', '0000000')
+UserResource.new(user).serialize
+# => '{"user":{"id":1,"address":{"city":"Tokyo","zipcode":"0000000"}}}'
+```
+
+Nested attributes can be nested deeply.
+
+```ruby
+class FooResource
+ include Alba::Resource
+
+ root_key :foo
+
+ nested :bar do
+ nested :baz do
+ attribute :deep do
+ 42
+ end
+ end
+ end
+end
+
+FooResource.new(nil).serialize
+# => '{"foo":{"bar":{"baz":{"deep":42}}}}'
+```
+
### Inline definition with `Alba.serialize`
`Alba.serialize` method is a shortcut to define everything inline.
```ruby
@@ -333,12 +517,62 @@
# => Same as `FooResource.new(something).serialize` when `something` is an instance of `Foo`.
```
Although this might be useful sometimes, it's generally recommended to define a class for Resource.
-### Inheritance and attributes filter
+### Serializable Hash
+Instead of serializing to JSON, you can also output a Hash by calling `serializable_hash` or the `to_h` alias. Note also that the `serialize` method is aliased as `to_json`.
+
+```ruby
+# These are equivalent and will return serialized JSON
+UserResource.new(user).serialize
+UserResource.new(user).to_json
+
+# These are equivalent and will return a Hash
+UserResource.new(user).serializable_hash
+UserResource.new(user).to_h
+```
+
+If you want a Hash that corresponds to a JSON String returned by `serialize` method, you can use `as_json`.
+
+```ruby
+# These are equivalent and will return the same result
+UserResource.new(user).serialize
+UserResource.new(user).to_json
+JSON.generate(UserResource.new(user).as_json)
+```
+
+### Inheritance
+
+When you include `Alba::Resource` in your class, it's just a class so you can define any class that inherits from it. You can add new attributes to inherited class like below:
+
+```ruby
+class FooResource
+ include Alba::Resource
+
+ root_key :foo
+
+ attributes :bar
+end
+
+class ExtendedFooResource < FooResource
+ root_key :foofoo
+
+ attributes :baz
+end
+
+Foo = Struct.new(:bar, :baz)
+foo = Foo.new(1, 2)
+FooResource.new(foo).serialize # => '{"foo":{"bar":1}}'
+ExtendedFooResource.new(foo).serialize # => '{"foo":{"bar":1,"baz":2}}'
+```
+
+In this example we add `baz` attribute and change `root_key`. This way, you can extend existing resource classes just like normal OOP. Don't forget that when your inheritance structure is too deep it'll become difficult to modify existing classes.
+
+### Filtering attributes
+
You can filter out certain attributes by overriding `attributes` instance method. This is useful when you want to customize existing resource with inheritance.
You can access raw attributes via `super` call. It returns a Hash whose keys are the name of the attribute and whose values are the body. Usually you need only keys to filter out, like below.
```ruby
@@ -406,10 +640,12 @@
* `:lower_camel` for lowerCamelCase
* `:dash` for dash-case
* `:snake` for snake_case
* `:none` for not transforming keys
+#### Root key transformation
+
You can also transform root key when:
* `Alba.enable_inference!` is called
* `root_key!` is called in Resource class
* `root` option of `transform_keys` is set to true
@@ -439,78 +675,85 @@
# => '{"bank-account":{"account-number":123456789}}'
```
This behavior to transform root key will become default at version 2.
-Supported transformation types are :camel, :lower_camel and :dash.
+#### Key transformation cascading
-#### Custom inflector
+When you use `transform_keys` with inline association, it automatically applies the same transformation type to those inline association.
-A custom inflector can be plugged in as follows.
+This is the default behavior from version 2, but you can do the same thing with adding `transform_keys` to each association.
+You can also turn it off by setting `cascade: false` option to `transform_keys`.
+
```ruby
-module CustomInflector
- module_function
+class User
+ attr_reader :id, :first_name, :last_name
- def camelize(string)
+ def initialize(id, first_name, last_name)
+ @id = id
+ @first_name = first_name
+ @last_name = last_name
+ @bank_account = BankAccount.new(1234)
end
+end
- def camelize_lower(string)
- end
+class BankAccount
+ attr_reader :account_number
- def dasherize(string)
+ def initialize(account_number)
+ @account_number = account_number
end
+end
- def underscore(string)
- end
+class UserResource
+ include Alba::Resource
- def classify(string)
+ attributes :id, :first_name, :last_name
+
+ transform_keys :lower_camel # Default is cascade: true
+
+ one :bank_account do
+ attributes :account_number
end
end
-Alba.enable_inference!(with: CustomInflector)
+user = User.new(1, 'Masafumi', 'Okura')
+UserResource.new(user).serialize
+# => '{"id":1,"firstName":"Masafumi","lastName":"Okura","bankAccount":{"accountNumber":1234}}'
```
-### Filtering attributes
+#### Custom inflector
-You can filter attributes by overriding `Alba::Resource#converter` method, but it's a bit tricky.
+A custom inflector can be plugged in as follows.
```ruby
-class User
- attr_accessor :id, :name, :email, :created_at, :updated_at
+module CustomInflector
+ module_function
- def initialize(id, name, email)
- @id = id
- @name = name
- @email = email
+ def camelize(string)
end
-end
-class UserResource
- include Alba::Resource
+ def camelize_lower(string)
+ end
- attributes :id, :name, :email
+ def dasherize(string)
+ end
- private
+ def underscore(string)
+ end
- # Here using `Proc#>>` method to compose a proc from `super`
- def converter
- super >> proc { |hash| hash.compact }
+ def classify(string)
end
end
-user = User.new(1, nil, nil)
-UserResource.new(user).serialize # => '{"id":1}'
+Alba.enable_inference!(with: CustomInflector)
```
-The key part is the use of `Proc#>>` since `Alba::Resource#converter` returns a `Proc` which contains the basic logic and it's impossible to change its behavior by just overriding the method.
-
-It's not recommended to swap the whole conversion logic. It's recommended to always call `super` when you override `converter`.
-
### Conditional attributes
-Filtering attributes with overriding `convert` works well for simple cases. However, It's cumbersome when we want to filter various attributes based on different conditions for keys.
+Filtering attributes with overriding `attributes` works well for simple cases. However, It's cumbersome when we want to filter various attributes based on different conditions for keys.
In these cases, conditional attributes works well. We can pass `if` option to `attributes`, `attribute`, `one` and `many`. Below is an example for the same effect as [filtering attributes section](#filtering-attributes).
```ruby
class User
@@ -759,11 +1002,11 @@
**Note that this feature works correctly since version 1.3. In previous versions it doesn't work as expected.**
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.
-For more details, please refer to [test code](https://github.com/okuramasafumi/alba/blob/master/test/usecases/circular_association_test.rb)
+For more details, please refer to [test code](https://github.com/okuramasafumi/alba/blob/main/test/usecases/circular_association_test.rb)
### Experimental support of types
You can validate and convert input with types.
@@ -802,10 +1045,41 @@
# => TypeError, 'Attribute bio is expected to be String but actually nil.'
```
Note that this feature is experimental and interfaces are subject to change.
+### Collection serialization into Hash
+
+Sometimes we want to serialize a collection into a Hash, not an Array. It's possible with Alba.
+
+```ruby
+class User
+ attr_reader :id, :name
+ def initialize(id, name)
+ @id, @name = id, name
+ end
+end
+
+class UserResource
+ include Alba::Resource
+
+ collection_key :id # This line is important
+
+ attributes :id, :name
+end
+
+user1 = User.new(1, 'John')
+user2 = User.new(2, 'Masafumi')
+
+UserResource.new([user1, user2]).serialize
+# => '{"1":{"id":1,"name":"John"},"2":{"id":2,"name":"Masafumi"}}'
+```
+
+In the snippet above, `collection_key :id` specifies the key used for the key of the collection hash. In this example it's `:id`.
+
+Make sure that collection key is unique for the collection.
+
### Layout
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".
Alba supports serializing JSON in a layout. You need a file for layout and then to specify file with `layout` method.
@@ -970,20 +1244,70 @@
end
```
In this way we have shorter and cleaner code. Note that we need to use `send` or `public_send` in `attribute` block to get attribute data.
+### Debugging
+
+Debugging is not easy. If you find Alba not working as you expect, there are a few things to do:
+
+1. Inspect
+
+The typical code looks like this:
+
+```ruby
+class FooResource
+ include Alba::Resource
+ attributes :id
+end
+FooResource.new(foo).serialize
+```
+
+Notice that we instantiate `FooResource` and then call `serialize` method. We can get various information by calling `inspect` method on it.
+
+```ruby
+puts FooResource.new(foo).inspect # or: p class FooResource.new(foo)
+# => "#<FooResource:0x000000010e21f408 @object=[#<Foo:0x000000010e3470d8 @id=1>], @params={}, @within=#<Object:0x000000010df2eac8>, @method_existence={}, @_attributes={:id=>:id}, @_key=nil, @_key_for_collection=nil, @_meta=nil, @_transform_type=:none, @_transforming_root_key=false, @_on_error=nil, @_on_nil=nil, @_layout=nil, @_collection_key=nil>"
+```
+
+The output might be different depending on the version of Alba or the object you give, but the concepts are the same. `@object` represents the object you gave as an argument to `new` method, `@_attributes` represents the attributes you defined in `FooResource` class using `attributes` DSL.
+
+Other things are not so important, but you need to take care of corresponding part when you use additional features such as `root_key`, `transform_keys` and adding params.
+
+2. Logging
+
+Alba currently doesn't support logging directly, but you can add your own logging module to Alba easily.
+
+```ruby
+module Logging
+ def serialize(...) # `...` was added in Ruby 2.7
+ puts serializable_hash
+ super(...)
+ end
+end
+
+FooResource.prepend Logging
+FooResource.new(foo).serialize
+# => "{:id=>1}" is printed
+```
+
+Here, we override `serialize` method with `prepend`. In overridden method we print the result of `serializable_hash` that gives the basic hash for serialization to `serialize` method. Using `...` allows us to override without knowing method signiture of `serialize`.
+
+Don't forget calling `super` in this way.
+
## Rails
When you use Alba in Rails, you can create an initializer file with the line below for compatibility with Rails JSON encoder.
```ruby
Alba.backend = :active_support
# or
Alba.backend = :oj_rails
```
+To find out more details, please see https://github.com/okuramasafumi/alba/blob/main/docs/rails.md
+
## Why named "Alba"?
The name "Alba" comes from "albatross", a kind of birds. In Japanese, this bird is called "Aho-dori", which means "stupid bird". I find it funny because in fact albatrosses fly really fast. I hope Alba looks stupid but in fact it does its job quick.
## Pioneers
@@ -993,21 +1317,20 @@
* [ActiveModelSerializers](https://github.com/rails-api/active_model_serializers) a.k.a AMS, the most famous implementation of JSON serializer for Ruby
* [Blueprinter](https://github.com/procore/blueprinter) shares some concepts with Alba
## Development
-After checking out the repo, run `bin/setup` to install dependencies. Then, run `rake test` to run the tests. You can also run `bin/console` for an interactive prompt that will allow you to experiment.
+After checking out the repo, run `bin/setup` to install dependencies. Then, run `bundle exec rake test` to run the tests. You can also run `bin/console` for an interactive prompt that will allow you to experiment.
To install this gem onto your local machine, run `bundle exec rake install`. To release a new version, update the version number in `version.rb`, and then run `bundle exec rake release`, which will create a git tag for the version, push git commits and tags, and push the `.gem` file to [rubygems.org](https://rubygems.org).
## Contributing
-Bug reports and pull requests are welcome on GitHub at https://github.com/okuramasafumi/alba. This project is intended to be a safe, welcoming space for collaboration, and contributors are expected to adhere to the [code of conduct](https://github.com/okuramasafumi/alba/blob/master/CODE_OF_CONDUCT.md).
+Thank you for begin interested in contributing to Alba! Please see [contributors guide](https://github.com/okuramasafumi/alba/blob/main/CONTRIBUTING.md) before start contributing. If you have any questions, please feel free to ask in [Discussions](https://github.com/okuramasafumi/alba/discussions).
-
## License
The gem is available as open source under the terms of the [MIT License](https://opensource.org/licenses/MIT).
## Code of Conduct
-Everyone interacting in the Alba project's codebases, issue trackers, chat rooms and mailing lists is expected to follow the [code of conduct](https://github.com/okuramasafumi/alba/blob/master/CODE_OF_CONDUCT.md).
+Everyone interacting in the Alba project's codebases, issue trackers, chat rooms and mailing lists is expected to follow the [code of conduct](https://github.com/okuramasafumi/alba/blob/main/CODE_OF_CONDUCT.md).