README.md in alba-2.0.1 vs README.md in alba-2.1.0
- old
+ new
@@ -35,11 +35,11 @@
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.
### Feature rich
-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).
+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), [root key and association resource name inference](#root-key-and-association-resource-name-inference) and [supports layouts](#layout).
## Installation
Add this line to your application's Gemfile:
@@ -55,25 +55,24 @@
$ gem install alba
## Supported Ruby versions
-Alba supports CRuby 2.5 and higher and latest JRuby and TruffleRuby.
+Alba supports CRuby 2.6 and higher and latest JRuby and TruffleRuby.
## Documentation
You can find the documentation on [RubyDoc](https://rubydoc.info/github/okuramasafumi/alba).
## Features
* Conditional attributes and associations
* Selectable backend
* Key transformation
-* Root key inference
+* Root key and association resource name inference
* Error handling
* Nil handling
-* Resource name inflection based on association name
* Circular associations control
* [Experimental] Types for validation and conversion
* Layout
* No runtime dependencies
@@ -111,24 +110,38 @@
You can consider setting a backend with Symbol as a shortcut to set encoder.
#### Inference configuration
-You can enable inference feature using `enable_inference!` method.
+You can enable the inference feature using the `Alba.inflector = SomeInflector` API. For example, in a Rails initializer:
```ruby
-Alba.enable_inference!(with: :active_support)
+Alba.inflector = :active_support
```
-You can choose which inflector Alba uses for inference. Possible value for `with` option are:
+You can choose which inflector Alba uses for inference. Possible options are:
- `:active_support` for `ActiveSupport::Inflector`
- `:dry` for `Dry::Inflector`
-- any object which responds to some methods (see [below](#custom-inflector))
+- any object which conforms to the protocol (see [below](#custom-inflector))
-For the details, see [Error handling section](#error-handling)
+To disable inference, set the `inflector` to `nil`:
+```ruby
+Alba.inflector = nil
+```
+
+To check if inference is enabled etc, inspect the return value of `inflector`:
+
+```ruby
+if Alba.inflector == nil
+ puts "inflector not set"
+else
+ puts "inflector is set to #{Alba.inflector}"
+end
+```
+
### Simple serialization with root key
You can define attributes with (yes) `attributes` macro with attribute names. If your attribute need some calculations, you can use `attribute` with block.
```ruby
@@ -155,11 +168,11 @@
end
end
user = User.new(1, 'Masafumi OKURA', 'masafumi@example.com')
UserResource.new(user).serialize
-# => "{\"user\":{\"id\":1,\"name\":\"Masafumi OKURA\",\"name_with_email\":\"Masafumi OKURA: masafumi@example.com\"}}"
+# => '{"user":{"id":1,"name":"Masafumi OKURA","name_with_email":"Masafumi OKURA: masafumi@example.com"}}'
```
You can define instance methods on resources so that you can use it as attribute name in `attributes`.
```ruby
@@ -182,11 +195,11 @@
```ruby
user1 = User.new(1, 'Masafumi OKURA', 'masafumi@example.com')
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\"}]}"
+# => '{"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
@@ -208,12 +221,12 @@
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\"}"
+UserResource.new(user).serialize # => '{"name":"Masa"}'
+UserResource.new(user, params: {upcase: true}).serialize # => '{"name":"MASA"}'
```
### 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.
@@ -366,13 +379,15 @@
end
UserResource.new(user).serialize
# => '{"id":1,"my_articles":[{"title":"Hello World!"}]}'
```
-You can omit resource option if you enable Alba's inference feature.
+You can omit the resource option if you enable Alba's [inference](#inference-configuration) feature.
```ruby
+Alba.inflector = :active_support
+
class UserResource
include Alba::Resource
attributes :id
@@ -388,11 +403,11 @@
class UserResource
include Alba::Resource
attributes :id
- many :articles, ->(article) { article.with_comment? ? ArticleWithCommentResource : ArticleResource }
+ many :articles, resource: ->(article) { article.with_comment? ? ArticleWithCommentResource : ArticleResource }
end
```
Note that using a Proc slows down serialization if there are too `many` associated objects.
@@ -562,17 +577,23 @@
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}}'
+ExtendedFooResource.new(foo).serialize # => '{"foofoo":{"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
+Filtering attributes can be done in two ways - with `attributes` and `select`. They have different semantics and usage.
+
+`select` is a new and more intuitive API, so generally it's recommended to use `select`.
+
+#### Filtering attributes with `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
@@ -596,23 +617,56 @@
def attributes
super.select { |key, _| key.to_sym == :name }
end
end
+foo = Foo.new(1, 'my foo', 'body')
+
RestrictedFooResource.new(foo).serialize
# => '{"name":"my foo"}'
```
-### Key transformation
+#### Filtering attributes with `select`
-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:
-* install it
-* use a [custom inflector](#custom-inflector)
+When you want to filter attributes based on more complex logic, you can use `select` instance method. `select` takes two parameters, the name of an attribute and the value of an attribute. If it returns false that attribute is rejected.
-With `transform_keys` DSL, you can transform attribute keys.
+```ruby
+class Foo
+ attr_accessor :id, :name, :body
+ def initialize(id, name, body)
+ @id = id
+ @name = name
+ @body = body
+ end
+end
+
+class GenericFooResource
+ include Alba::Resource
+
+ attributes :id, :name, :body
+end
+
+class RestrictedFooResource < GenericFooResource
+ def select(_key, value)
+ !value.nil?
+ end
+end
+
+foo = Foo.new(1, nil, 'body')
+
+RestrictedFooResource.new(foo).serialize
+# => '{"id":1,"body":"body"}'
+```
+
+### Key transformation
+
+If you have [inference](#inference-configuration) enabled, you can use the `transform_keys` DSL to transform attribute keys.
+
```ruby
+Alba.inflector = :active_support
+
class User
attr_reader :id, :first_name, :last_name
def initialize(id, first_name, last_name)
@id = id
@@ -644,16 +698,16 @@
#### Root key transformation
You can also transform root key when:
-* `Alba.enable_inference!` is called
+* `Alba.inflector` is set
* `root_key!` is called in Resource class
* `root` option of `transform_keys` is set to true
```ruby
-Alba.enable_inference!(with: :active_support) # with :dry also works
+Alba.inflector = :active_support
class BankAccount
attr_reader :account_number
def initialize(account_number)
@@ -673,23 +727,25 @@
bank_account = BankAccount.new(123_456_789)
BankAccountResource.new(bank_account).serialize
# => '{"bank-account":{"account-number":123456789}}'
```
-This behavior to transform root key will become default at version 2.
+This is the default behavior from version 2.
+Find more details in the [Inference configuration](#inference-configuration) section.
+
#### Key transformation cascading
When you use `transform_keys` with inline association, it automatically applies the same transformation type to those inline association.
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
class User
- attr_reader :id, :first_name, :last_name
+ attr_reader :id, :first_name, :last_name, :bank_account
def initialize(id, first_name, last_name)
@id = id
@first_name = first_name
@last_name = last_name
@@ -744,22 +800,22 @@
def classify(string)
end
end
-Alba.enable_inference!(with: CustomInflector)
+Alba.inflector = CustomInflector
```
### Conditional attributes
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
- attr_accessor :id, :name, :email, :created_at, :updated_at
+ attr_accessor :id, :name, :email
def initialize(id, name, email)
@id = id
@name = name
@email = email
@@ -788,16 +844,16 @@
end
```
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.)
-### Inference
+### Root key and association resource name inference
-After `Alba.enable_inference!` called, Alba tries to infer root key and association resource name.
+If [inference](#inference-configuration) is enabled, Alba tries to infer the root key and association resource names.
```ruby
-Alba.enable_inference!(with: :active_support) # with :dry also works
+Alba.inflector = :active_support
class User
attr_reader :id
attr_accessor :articles
@@ -823,11 +879,11 @@
end
class UserResource
include Alba::Resource
- key!
+ root_key!
attributes :id
many :articles
end
@@ -841,10 +897,12 @@
This resource automatically sets its root key to either "users" or "user", depending on the given object is collection or not.
Also, you don't have to specify which resource class to use with `many`. Alba infers it from association name.
+Find more details in the [Inference configuration](#inference-configuration) section.
+
### Error handling
You can set error handler globally or per resource using `on_error`.
```ruby
@@ -932,11 +990,11 @@
```ruby
class UserResource
include Alba::Resource
on_nil do |object, key|
- if key == age
+ if key == 'age'
20
else
"User#{object.id}"
end
end
@@ -988,10 +1046,10 @@
root_key :user, :users
attributes :id, :name
end
-UserResource.new([user]).serialize(meta: {foo: :bar})
+UserResourceWithoutMeta.new([user]).serialize(meta: {foo: :bar})
# => '{"users":[{"id":1,"name":"Masafumi OKURA"}],"meta":{"foo":"bar"}}'
```
You can use `object` method to access the underlying object and `params` to access the params in `meta` block.