README.md in alba-1.5.0 vs README.md in alba-1.6.0

- old
+ new

@@ -1,10 +1,9 @@ [![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) [![Maintainability](https://api.codeclimate.com/v1/badges/fdab4cc0de0b9addcfe8/maintainability)](https://codeclimate.com/github/okuramasafumi/alba/maintainability) -[![Inline docs](http://inch-ci.org/github/okuramasafumi/alba.svg?branch=main)](http://inch-ci.org/github/okuramasafumi/alba) ![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 @@ -18,23 +17,23 @@ 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! ## Why Alba? -Because it's fast, flexible and well-maintained! +Because it's fast, easy-to-use and extensible! ### Fast Alba is faster than most of the alternatives. We have a [benchmark](https://github.com/okuramasafumi/alba/tree/master/benchmark). -### Flexible +### Easy to use -Alba provides a small set of DSL to define your serialization logic. It also provides methods you can override to alter and filter serialized hash so that you have full control over the result. +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. -### Maintained +### Extensible -Alba is well-maintained and adds features quickly. [Coverage Status](https://coveralls.io/github/okuramasafumi/alba?branch=master) and [CodeClimate Maintainability](https://codeclimate.com/github/okuramasafumi/alba/maintainability) show the code base is quite healthy. +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. ## Installation Add this line to your application's Gemfile: @@ -70,19 +69,10 @@ * Circular associations control * [Experimental] Types for validation and conversion * Layout * No runtime dependencies -## Anti features - -* Sorting keys -* Class level support of parameters -* Supporting all existing JSON encoder/decoder -* Cache -* [JSON:API](https://jsonapi.org) support -* And many others - ## Usage ### Configuration Alba's configuration is fairly simple. @@ -114,27 +104,25 @@ #### Inference configuration You can enable inference feature using `enable_inference!` method. ```ruby -Alba.enable_inference! +Alba.enable_inference!(with: :active_support) ``` -You must install `ActiveSupport` to enable inference. +You can choose which inflector Alba uses for inference. Possible value for `with` option are: -#### Error handling configuration +- `:active_support` for `ActiveSupport::Inflector` +- `:dry` for `Dry::Inflector` +- any object which responds to some methods (see [below](#custom-inflector)) -You can configure error handling with `on_error` method. - -```ruby -Alba.on_error :ignore -``` - For the details, see [Error handling section](#error-handling) ### 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 class User attr_accessor :id, :name, :email, :created_at, :updated_at def initialize(id, name, email) @id = id @@ -157,13 +145,40 @@ end end user = User.new(1, 'Masafumi OKURA', 'masafumi@example.com') UserResource.new(user).serialize -# => "{\"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 +# The serialization result is the same as above +class UserResource + include Alba::Resource + + root_key :user, :users # Later is for plural + + attributes :id, :name, :name_with_email + + # Attribute methods must accept one argument for each serialized object + def name_with_email(user) + "#{user.name}: #{user.email}" + end +end +``` + +This even works with users collection. + +```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\"}]}" +``` + ### Serialization with associations ```ruby class User attr_reader :id, :created_at, :updated_at @@ -238,10 +253,67 @@ attributes :title end end ``` +You can "filter" association using second proc argument. This proc takes association object and `params`. + +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_accessor :articles + + def initialize(id) + @id = id + @articles = [] + end +end + +class Article + attr_accessor :id, :title, :body + + def initialize(id, title, body) + @id = id + @title = title + @body = body + end +end + +class ArticleResource + include Alba::Resource + + attributes :title +end + +class UserResource + include Alba::Resource + + attributes :id + + # Second proc works as a filter + many :articles, + proc { |articles, params| + filter = params[:filter] || :odd? + articles.select {|a| a.id.send(filter) } + }, + resource: ArticleResource +end + +user = User.new(1) +article1 = Article.new(1, 'Hello World!', 'Hello World!!!') +user.articles << article1 +article2 = Article.new(2, 'Super nice', 'Really nice!') +user.articles << article2 + +UserResource.new(user).serialize +# => '{"id":1,"articles":[{"title":"Hello World!"}]}' +UserResource.new(user, params: {filter: :even?}).serialize +# => '{"id":1,"articles":[{"title":"Super nice"}]}' +``` + ### Inline definition with `Alba.serialize` `Alba.serialize` method is a shortcut to define everything inline. ```ruby @@ -261,14 +333,16 @@ # => 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 Ignorance +### Inheritance and attributes filter -You can `exclude` or `ignore` certain attributes using `ignoring`. +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 class Foo attr_accessor :id, :name, :body def initialize(id, name, body) @@ -283,11 +357,13 @@ attributes :id, :name, :body end class RestrictedFooResource < GenericFooResource - ignoring :id, :body + def attributes + super.select { |key, _| key.to_sym == :name } + end end RestrictedFooResource.new(foo).serialize # => '{"name":"my foo"}' ``` @@ -322,18 +398,26 @@ user = User.new(1, 'Masafumi', 'Okura') UserResourceCamel.new(user).serialize # => '{"id":1,"firstName":"Masafumi","lastName":"Okura"}' ``` +Possible values for `transform_keys` argument are: + +* `:camel` for CamelCase +* `:lower_camel` for lowerCamelCase +* `:dash` for dash-case +* `:snake` for snake_case +* `:none` for not transforming keys + 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 or `Alba.enable_root_key_transformation!` is called. +* `root` option of `transform_keys` is set to true ```ruby -Alba.enable_inference! +Alba.enable_inference!(with: :active_support) # with :dry also works class BankAccount attr_reader :account_number def initialize(account_number) @@ -359,35 +443,34 @@ Supported transformation types are :camel, :lower_camel and :dash. #### Custom inflector -A custom inflector can be plugged in as follows... +A custom inflector can be plugged in as follows. + ```ruby -Alba.inflector = MyCustomInflector -``` -...and has to implement following interface (the parameter `key` is of type `String`): -```ruby -module InflectorInterface - def camelize(key) - raise "Not implemented" +module CustomInflector + module_function + + def camelize(string) end - def camelize_lower(key) - raise "Not implemented" + def camelize_lower(string) end - def dasherize(key) - raise "Not implemented" + def dasherize(string) end + + def underscore(string) + end + + def classify(string) + end end +Alba.enable_inference!(with: CustomInflector) ``` -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: -```ruby -Alba.inflector = Dry::Inflector.new -``` ### Filtering attributes You can filter attributes by overriding `Alba::Resource#converter` method, but it's a bit tricky. @@ -467,11 +550,11 @@ ### Inference After `Alba.enable_inference!` called, Alba tries to infer root key and association resource name. ```ruby -Alba.enable_inference! +Alba.enable_inference!(with: :active_support) # with :dry also works class User attr_reader :id attr_accessor :articles @@ -515,12 +598,10 @@ 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. -Note that to enable this feature you must install `ActiveSupport` gem. - ### Error handling You can set error handler globally or per resource using `on_error`. ```ruby @@ -560,16 +641,18 @@ * Block gives you more control over what to be returned. The block receives five arguments, `error`, `object`, `key`, `attribute` and `resource class` and must return a two-element array. Below is an example. ```ruby -# Global error handling -Alba.on_error do |error, object, key, attribute, resource_class| - if resource_class == MyResource - ['error_fallback', object.error_fallback] - else - [key, error.message] +class ExampleResource + include Alba::Resource + on_error do |error, object, key, attribute, resource_class| + if resource_class == MyResource + ['error_fallback', object.error_fallback] + else + [key, error.message] + end end end ``` ### Nil handling @@ -622,47 +705,10 @@ UserResource.new(User.new(1)).serialize # => '{"user":{"id":1,"name":"User1","age":20}}' ``` -You can also set global nil handler. - -```ruby -Alba.on_nil { 'default name' } - -class Foo - attr_reader :name - def initialize(name) - @name = name - end -end - -class FooResource - include Alba::Resource - - key :foo - - attributes :name -end - -FooResource.new(Foo.new).serialize -# => '{"foo":{"name":"default name"}}' - -class FooResource2 - include Alba::Resource - - key :foo - - on_nil { '' } # This is applied instead of global handler - - attributes :name -end - -FooResource2.new(Foo.new).serialize -# => '{"foo":{"name":""}}' -``` - ### Metadata You can set a metadata with `meta` DSL or `meta` option. ```ruby @@ -821,9 +867,111 @@ 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. ### Caching 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). + +### Extend Alba + +Sometimes we have shared behaviors across resources. In such cases we can have a module for common logic. + +In `attribute` block we can call instance method so we can improve the code below: + +```ruby +class FooResource + include Alba::Resource + # other attributes + attribute :created_at do |foo| + foo.created_at.strftime('%m/%d/%Y') + end + + attribute :updated_at do |foo| + foo.updated_at.strftime('%m/%d/%Y') + end +end + +class BarResource + include Alba::Resource + # other attributes + attribute :created_at do |bar| + bar.created_at.strftime('%m/%d/%Y') + end + + attribute :updated_at do |bar| + bar.updated_at.strftime('%m/%d/%Y') + end +end +``` + +to: + +```ruby +module SharedLogic + def format_time(time) + time.strftime('%m/%d/%Y') + end +end + +class FooResource + include Alba::Resource + include SharedLogic + # other attributes + attribute :created_at do |foo| + format_time(foo.created_at) + end + + attribute :updated_at do |foo| + format_time(foo.updated_at) + end +end + +class BarResource + include Alba::Resource + include SharedLogic + # other attributes + attribute :created_at do |bar| + format_time(bar.created_at) + end + + attribute :updated_at do |bar| + format_time(bar.updated_at) + end +end +``` + +We can even add our own DSL to serialize attributes for readability and removing code duplications. + +To do so, we need to `extend` our module. Let's see how we can achieve the same goal with this approach. + +```ruby +module AlbaExtension + # Here attrs are an Array of Symbol + def formatted_time_attributes(*attrs) + attrs.each do |attr| + attribute attr do |object| + time = object.send(attr) + time.strftime('%m/%d/%Y') + end + end + end +end + +class FooResource + include Alba::Resource + extend AlbaExtension + # other attributes + formatted_time_attributes :created_at, :updated_at +end + +class BarResource + include Alba::Resource + extend AlbaExtension + # other attributes + formatted_time_attributes :created_at, :updated_at +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. ## Rails When you use Alba in Rails, you can create an initializer file with the line below for compatibility with Rails JSON encoder.