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.