GithubHelp home page GithubHelp logo

okuramasafumi / alba Goto Github PK

View Code? Open in Web Editor NEW
889.0 7.0 42.0 1.17 MB

Alba is a JSON serializer for Ruby, JRuby and TruffleRuby.

Home Page: https://okuramasafumi.github.io/alba/

License: MIT License

Ruby 99.82% Shell 0.08% HTML 0.09%
json-serializer json-serialization ruby performance json presenter hacktoberfest

alba's Introduction

alba card

Gem Version CI codecov Maintainability GitHub code size in bytes GitHub Contributor Covenant

Alba

Alba is a JSON serializer for Ruby, JRuby, and TruffleRuby.

IMPORTANT NOTICE

Both version 3.0.0 and 2.4.2 contain important bug fix. However, version 3.0.0 has some bugs (see #342). Until they get fixed, it's highly recommended to upgrade to version 2.4.2. Dependabot and similar tools might create an automated Pull Request to upgrade to 3.0.0, so it might be required to upgrade to 2.4.2 manually. Version 3.0.1 has been released so Ruby 3 users should upgrade to 3.0.1. For Ruby 2 users, it's highly recommended to upgrade to 2.4.2. Sorry for the inconvenience.

TL;DR

Alba allows you to do something like below.

class User
  attr_accessor :id, :name, :email

  def initialize(id, name, email)
    @id = id
    @name = name
    @email = email
  end
end

class UserResource
  include Alba::Resource

  root_key :user

  attributes :id, :name

  attribute :name_with_email do |resource|
    "#{resource.name}: #{resource.email}"
  end
end

user = User.new(1, 'Masafumi OKURA', '[email protected]')
UserResource.new(user).serialize
# => '{"user":{"id":1,"name":"Masafumi OKURA","name_with_email":"Masafumi OKURA: [email protected]"}}'

Seems useful? Continue reading!

Discussions

Alba uses GitHub Discussions to openly discuss the project.

If you've already used Alba, please consider posting your thoughts and feelings on 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. Let's make Alba even better, together!

Resources

If you want to know more about Alba, there's a screencast created by Sebastian from Hanami Mastery. It covers basic features of Alba and how to use it in Hanami.

What users say about Alba

Alba is a well-maintained JSON serialization engine, for Ruby, JRuby, and TruffleRuby implementations, and what I like in this gem - except of its speed, is the easiness of use, no dependencies and the fact it plays well with any Ruby application!

Hanami Mastery by Seb Wilgosz

Alba is more feature-rich and pretty fast, too

Gemfile of dreams by Evil Martians

Why Alba?

Because it's fast, easy and feature rich!

Fast

Alba is faster than most of the alternatives. We have a benchmark.

Easy

Alba is easy to use because there are only a few methods to remember. It's also easy to understand due to clean and small 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, root key and association resource name inference and supports layouts.

Other reasons

  • Dependency free, no need to install oj or activesupport while Alba works well with them
  • Well tested, the test coverage is 99%
  • Well maintained, gettings frequent update and new releases (see version history)

Installation

Add this line to your application's Gemfile:

gem 'alba'

And then execute:

$ bundle install

Or install it yourself as:

$ gem install alba

Supported Ruby versions

Alba supports CRuby 3.0 and higher and latest JRuby and TruffleRuby.

Documentation

You can find the documentation on RubyDoc.

Features

  • Conditional attributes and associations
  • Selectable backend
  • Key transformation
  • Root key and association resource name inference
  • Inline definition without explicit classes
  • Error handling
  • Nil handling
  • Circular associations control
  • Types for validation and conversion
  • Layout
  • No runtime dependencies

Usage

Configuration

Alba's configuration is fairly simple.

Backend configuration

Backend is the actual part serializing an object into JSON. Alba supports these backends.

name description requires_external_gem encoder
oj, oj_strict Using Oj in strict mode Yes(C extension) Oj.dump(object, mode: :strict)
oj_rails It's oj but in rails mode Yes(C extension) Oj.dump(object, mode: :rails)
oj_default It's oj but respects mode set by users Yes(C extension) Oj.dump(object)
active_support For Rails compatibility Yes ActiveSupport::JSON.encode(object)
default, json Using json gem No JSON.generate(object)

You can set a backend like this:

Alba.backend = :oj

This is equivalent as:

Alba.encoder = ->(object) { Oj.dump(object, mode: :strict) }

Encoder configuration

You can also set JSON encoder directly with a Proc.

Alba.encoder = ->(object) { JSON.generate(object) }

You can consider setting a backend with Symbol as a shortcut to set encoder.

Inference configuration

You can enable the inference feature using the Alba.inflector = SomeInflector API. For example, in a Rails initializer:

Alba.inflector = :active_support

You can choose which inflector Alba uses for inference. Possible options are:

  • :active_support for ActiveSupport::Inflector
  • :dry for Dry::Inflector
  • any object which conforms to the protocol (see below)

To disable inference, set the inflector to nil:

Alba.inflector = nil

To check if inference is enabled etc, inspect the return value of inflector:

if Alba.inflector.nil?
  puts 'inflector not set'
else
  puts "inflector is set to #{Alba.inflector}"
end

Naming

Alba tries to infer resource name from class name like the following.

Class name Resource name
FooResource Foo
FooSerializer Foo
FooElse FooElse

Resource name is used as the default name of the root key, so you might want to name it ending with "Resource" or "Serializer"

When you use Alba with Rails, it's recommended to put your resource/serializer classes in corresponding directory such as app/resources or app/serializers.

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.

class User
  attr_accessor :id, :name, :email, :created_at, :updated_at

  def initialize(id, name, email)
    @id = id
    @name = name
    @email = email
    @created_at = Time.now
    @updated_at = Time.now
  end
end

class UserResource
  include Alba::Resource

  root_key :user

  attributes :id, :name

  attribute :name_with_email do |resource|
    "#{resource.name}: #{resource.email}"
  end
end

user = User.new(1, 'Masafumi OKURA', '[email protected]')
UserResource.new(user).serialize
# => '{"user":{"id":1,"name":"Masafumi OKURA","name_with_email":"Masafumi OKURA: [email protected]"}}'

You can define instance methods on resources so that you can use it as attribute name in attributes.

# 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.

user1 = User.new(1, 'Masafumi OKURA', '[email protected]')
user2 = User.new(2, 'Test User', '[email protected]')
UserResource.new([user1, user2]).serialize
# => '{"users":[{"id":1,"name":"Masafumi OKURA","name_with_email":"Masafumi OKURA: [email protected]"},{"id":2,"name":"Test User","name_with_email":"Test User: [email protected]"}]}'

If you have a simple case where you want to change only the name, you can use the Symbol to Proc shortcut:

class UserResource
  include Alba::Resource

  attribute :some_other_name, &:name
end

Methods conflict

Consider following code:

class Foo
  def bar
    'This is Foo'
  end
end

class FooResource
  include Alba::Resource

  attributes :bar

  def bar
    'This is FooResource'
  end
end

FooResource.new(Foo.new).serialize

By default, Alba create the JSON as '{"bar":"This is FooResource"}'. This means Alba calls a method on a Resource class and doesn't call a method on a target object. This rule is applied to methods that are explicitly defined on Resource class, so methods that Resource class inherits from Object class such as format are ignored.

class Foo
  def format
    'This is Foo'
  end
end

class FooResource
  include Alba::Resource

  attributes :bar

  # Here, `format` method is available
end

FooResource.new(Foo.new).serialize
# => '{"bar":"This is Foo"}'

If you'd like Alba to call methods on a target object, use prefer_object_method! like below.

class Foo
  def bar
    'This is Foo'
  end
end

class FooResource
  include Alba::Resource

  prefer_object_method! # <- important

  attributes :bar

  # This is not called
  def bar
    'This is FooResource'
  end
end

FooResource.new(Foo.new).serialize
# => '{"bar":"This is Foo"}'

Params

You can pass a Hash to the resource for internal use. It can be used as "flags" to control attribute content.

class UserResource
  include Alba::Resource
  attribute :name do |user|
    params[:upcase] ? user.name.upcase : user.name
  end
end

user = User.new(1, 'Masa', '[email protected]')
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.

class User
  attr_reader :id, :created_at, :updated_at
  attr_accessor :articles

  def initialize(id)
    @id = id
    @created_at = Time.now
    @updated_at = Time.now
    @articles = []
  end
end

class Article
  attr_accessor :user_id, :title, :body

  def initialize(user_id, title, body)
    @user_id = user_id
    @title = title
    @body = body
  end
end

class ArticleResource
  include Alba::Resource

  attributes :title
end

class UserResource
  include Alba::Resource

  attributes :id

  many :articles, 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!"},{"title":"Super nice"}]}'

You can define associations inline if you don't need a class for association.

class ArticleResource
  include Alba::Resource

  attributes :title
end

class UserResource
  include Alba::Resource

  attributes :id

  many :articles, resource: ArticleResource
end

# This class works the same as `UserResource`
class AnotherUserResource
  include Alba::Resource

  attributes :id

  many :articles do
    attributes :title
  end
end

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.

class User
  attr_reader :id, :banned
  attr_accessor :articles

  def initialize(id, banned = false)
    @id = id
    @banned = banned
    @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, user|
         filter = params[:filter] || :odd?
         articles.select { |a| a.id.__send__(filter) && !user.banned }
       },
       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"}]}'

You can change a key for association with key option.

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 the resource option if you enable Alba's inference feature.

Alba.inflector = :active_support

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.

class UserResource
  include Alba::Resource

  attributes :id

  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.

Params override

Associations can override params. This is useful when associations are deeply nested.

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).

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', '[email protected]', 'Tokyo', '0000000')
UserResource.new(user).serialize
# => '{"user":{"id":1,"address":{"city":"Tokyo","zipcode":"0000000"}}}'

Nested attributes can be nested deeply.

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.

Alba.serialize(user, root_key: :foo) do
  attributes :id
  many :articles do
    attributes :title, :body
  end
end
# => '{"foo":{"id":1,"articles":[{"title":"Hello World!","body":"Hello World!!!"},{"title":"Super nice","body":"Really nice!"}]}}'

Alba.serialize can be used when you don't know what kind of object you serialize. For example:

Alba.serialize(something)
# => 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.

Inline definition for multiple root keys

While Alba doesn't directly support multiple root keys, you can simulate it with Alba.serialize.

# Define foo and bar local variables here

Alba.serialize do
  attribute :key1 do
    FooResource.new(foo).to_h
  end

  attribute :key2 do
    BarResource.new(bar).to_h
  end
end
# => JSON containing "key1" and "key2" as root keys

Note that we must use to_h, not serialize, with resources.

We can also generate a JSON with multiple root keys without making any class by the combination of Alba.serialize and Alba.hashify.

# Define foo and bar local variables here

Alba.serialize do
  attribute :foo do
    Alba.hashify(foo) do
      attributes :id, :name # For example
    end
  end

  attribute :bar do
    Alba.hashify(bar) do
      attributes :id
    end
  end
end
# => JSON containing "foo" and "bar" as root keys

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.

# 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.

# 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:

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 # => '{"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.

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 attributes
    super.select { |key, _| key.to_sym == :name }
  end
end

foo = Foo.new(1, 'my foo', 'body')

RestrictedFooResource.new(foo).serialize
# => '{"name":"my foo"}'

Filtering attributes with select

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.

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 enabled, you can use the transform_keys DSL to transform attribute keys.

Alba.inflector = :active_support

class User
  attr_reader :id, :first_name, :last_name

  def initialize(id, first_name, last_name)
    @id = id
    @first_name = first_name
    @last_name = last_name
  end
end

class UserResource
  include Alba::Resource

  attributes :id, :first_name, :last_name

  transform_keys :lower_camel
end

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

Root key transformation

You can also transform root key when:

  • Alba.inflector is set
  • root_key! is called in Resource class
  • root option of transform_keys is set to true
Alba.inflector = :active_support

class BankAccount
  attr_reader :account_number

  def initialize(account_number)
    @account_number = account_number
  end
end

class BankAccountResource
  include Alba::Resource

  root_key!

  attributes :account_number
  transform_keys :dash, root: true
end

bank_account = BankAccount.new(123_456_789)
BankAccountResource.new(bank_account).serialize
# => '{"bank-account":{"account-number":123456789}}'

This is the default behavior from version 2.

Find more details in the 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.

class User
  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
    @bank_account = BankAccount.new(1234)
  end
end

class BankAccount
  attr_reader :account_number

  def initialize(account_number)
    @account_number = account_number
  end
end

class UserResource
  include Alba::Resource

  attributes :id, :first_name, :last_name

  transform_keys :lower_camel # Default is cascade: true

  one :bank_account do
    attributes :account_number
  end
end

user = User.new(1, 'Masafumi', 'Okura')
UserResource.new(user).serialize
# => '{"id":1,"firstName":"Masafumi","lastName":"Okura","bankAccount":{"accountNumber":1234}}'

Custom inflector

A custom inflector can be plugged in as follows.

module CustomInflector
  module_function

  def camelize(string); end

  def camelize_lower(string); end

  def dasherize(string); end

  def underscore(string); end

  def classify(string); end
end

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.

class User
  attr_accessor :id, :name, :email

  def initialize(id, name, email)
    @id = id
    @name = name
    @email = email
  end
end

class UserResource
  include Alba::Resource

  attributes :id, :name, :email, if: proc { |user, attribute| !attribute.nil? }
end

user = User.new(1, nil, nil)
UserResource.new(user).serialize # => '{"id":1}'

Default

Alba doesn't support default value for attributes, but it's easy to set a default value.

class FooResource
  attribute :bar do |foo|
    foo.bar || 'default bar'
  end
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.)

Root key and association resource name inference

If inference is enabled, Alba tries to infer the root key and association resource names.

Alba.inflector = :active_support

class User
  attr_reader :id
  attr_accessor :articles

  def initialize(id)
    @id = id
    @articles = []
  end
end

class Article
  attr_accessor :id, :title

  def initialize(id, title)
    @id = id
    @title = title
  end
end

class ArticleResource
  include Alba::Resource

  attributes :title
end

class UserResource
  include Alba::Resource

  root_key!

  attributes :id

  many :articles
end

user = User.new(1)
user.articles << Article.new(1, 'The title')

UserResource.new(user).serialize # => '{"user":{"id":1,"articles":[{"title":"The title"}]}}'
UserResource.new([user]).serialize # => '{"users":[{"id":1,"articles":[{"title":"The title"}]}]}'

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 section.

Error handling

You can set error handler globally or per resource using on_error.

class User
  attr_accessor :id, :name

  def initialize(id, name, email)
    @id = id
    @name = name
    @email = email
  end

  def email
    raise 'Error!'
  end
end

class UserResource
  include Alba::Resource

  attributes :id, :name, :email

  on_error :ignore
end

user = User.new(1, 'Test', '[email protected]')
UserResource.new(user).serialize # => '{"id":1,"name":"Test"}'

This way you can exclude an entry when fetching an attribute gives an exception.

There are four possible arguments on_error method accepts.

  • :raise re-raises an error. This is the default behavior.
  • :ignore ignores the entry with the error.
  • :nullify sets the attribute with the error to nil.
  • 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.

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

Sometimes we want to convert nil to different values such as empty string. Alba provides a flexible way to handle nil.

class User
  attr_reader :id, :name, :age

  def initialize(id, name = nil, age = nil)
    @id = id
    @name = name
    @age = age
  end
end

class UserResource
  include Alba::Resource

  on_nil { '' }

  root_key :user, :users

  attributes :id, :name, :age
end

UserResource.new(User.new(1)).serialize
# => '{"user":{"id":1,"name":"","age":""}}'

You can get various information via block parameters.

class UserResource
  include Alba::Resource

  on_nil do |object, key|
    if key == 'age'
      20
    else
      "User#{object.id}"
    end
  end

  root_key :user, :users

  attributes :id, :name, :age
end

UserResource.new(User.new(1)).serialize
# => '{"user":{"id":1,"name":"User1","age":20}}'

Note that on_nil does NOT work when the given object itself is nil. There are a few possible ways to deal with nil.

  • Use if statement and avoid using Alba when the object is nil
  • Use "Null Object" pattern

Metadata

You can set a metadata with meta DSL or meta option.

class UserResource
  include Alba::Resource

  root_key :user, :users

  attributes :id, :name

  meta do
    if object.is_a?(Enumerable)
      {size: object.size}
    else
      {foo: :bar}
    end
  end
end

user = User.new(1, 'Masafumi OKURA', '[email protected]')
UserResource.new([user]).serialize
# => '{"users":[{"id":1,"name":"Masafumi OKURA"}],"meta":{"size":1}}'

# You can merge metadata with `meta` option

UserResource.new([user]).serialize(meta: {foo: :bar})
# => '{"users":[{"id":1,"name":"Masafumi OKURA"}],"meta":{"size":1,"foo":"bar"}}'

You can change the key for metadata. If you change the key, it also affects the key when you pass meta option.

# You can change meta key
class UserResourceWithDifferentMetaKey
  include Alba::Resource

  root_key :user, :users

  attributes :id, :name

  meta :my_meta do
    {foo: :bar}
  end
end

UserResourceWithDifferentMetaKey.new([user]).serialize
# => '{"users":[{"id":1,"name":"Masafumi OKURA"}],"my_meta":{"foo":"bar"}}'

UserResourceWithDifferentMetaKey.new([user]).serialize(meta: {extra: 42})
# => '{"users":[{"id":1,"name":"Masafumi OKURA"}],"meta":{"size":1,"extra":42}}'

class UserResourceChangingMetaKeyOnly
  include Alba::Resource

  root_key :user, :users

  attributes :id, :name

  meta :my_meta
end

UserResourceChangingMetaKeyOnly.new([user]).serialize
# => '{"users":[{"id":1,"name":"Masafumi OKURA"}]}'

UserResourceChangingMetaKeyOnly.new([user]).serialize(meta: {extra: 42})
# => '{"users":[{"id":1,"name":"Masafumi OKURA"}],"my_meta":{"extra":42}}'

It's also possible to remove the key for metadata, resulting a flat structure.

class UserResourceRemovingMetaKey
  include Alba::Resource

  root_key :user, :users

  attributes :id, :name

  meta nil
end

UserResourceRemovingMetaKey.new([user]).serialize
# => '{"users":[{"id":1,"name":"Masafumi OKURA"}]}'

UserResourceRemovingMetaKey.new([user]).serialize(meta: {extra: 42})
# => '{"users":[{"id":1,"name":"Masafumi OKURA"}],"extra":42}'

# You can set metadata with `meta` option alone

class UserResourceWithoutMeta
  include Alba::Resource

  root_key :user, :users

  attributes :id, :name
end

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.

Note that setting root key is required when setting a metadata.

Circular associations control

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

Types

You can validate and convert input with types.

class User
  attr_reader :id, :name, :age, :bio, :admin, :created_at

  def initialize(id, name, age, bio = '', admin = false)
    @id = id
    @name = name
    @age = age
    @admin = admin
    @bio = bio
    @created_at = Time.new(2020, 10, 10)
  end
end

class UserResource
  include Alba::Resource

  attributes :name, id: [String, true], age: [Integer, true], bio: String, admin: [:Boolean, true], created_at: [String, ->(object) { object.strftime('%F') }]
end

user = User.new(1, 'Masafumi OKURA', '32', 'Ruby dev')
UserResource.new(user).serialize
# => '{"name":"Masafumi OKURA","id":"1","age":32,"bio":"Ruby dev","admin":false,"created_at":"2020-10-10"}'

Notice that id and created_at are converted to String and age is converted to Integer.

If type is not correct and auto conversion is disabled (default), TypeError occurs.

user = User.new(1, 'Masafumi OKURA', '32', nil) # bio is nil and auto conversion is disabled for bio
UserResource.new(user).serialize
# => TypeError, 'Attribute bio is expected to be String but actually nil.'

Custom types

You can define custom types to abstract data conversion logic. To define custom types, you can use Alba.register_type like below.

# Typically in initializer
Alba.register_type :iso8601, converter: ->(time) { time.iso8601(3) }, auto_convert: true

Then use it as regular types.

class UserResource
  include Alba::Resource

  attributes :id, created_at: :iso8601
end

You now get created_at attribute with iso8601 format!

Collection serialization into Hash

Sometimes we want to serialize a collection into a Hash, not an Array. It's possible with Alba.

class User
  attr_reader :id, :name

  def initialize(id, name)
    @id = id
    @name = 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.

{
  "header": "my_header",
  "body": <%= serialized_json %>
}
class FooResource
  include Alba::Resource
  layout file: 'my_layout.json.erb'
end

Note that layout files are treated as json and erb and evaluated in a context of the resource, meaning

  • A layout file must be a valid JSON
  • You must write <%= serialized_json %> in a layout to put serialized JSON string into a layout
  • You can access params in a layout so that you can add virtually any objects to a layout
    • 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
  • You can also access object, the underlying object for the resource

In case you don't want to have a file for layout, Alba lets you define and apply layouts inline:

class FooResource
  include Alba::Resource
  layout inline: proc {
    {
      header: 'my header',
      body: serializable_hash
    }
  }
end

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.

You can also use a Proc which returns String, not a Hash, for an inline layout.

class FooResource
  include Alba::Resource
  layout inline: proc {
    %({
      "header": "my header",
      "body": #{serialized_json}
    })
  }
end

It looks similar to file layout but you must use string interpolation for method calls since it's not an ERB.

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.

Helper

Inheritance works well in most of the cases to share behaviors. One of the exceptions is when you want to shared behaviors with inline association. For example:

class ApplicationResource
  include Alba::Resource

  def self.with_id
    attributes(:id)
  end
end

class LibraryResource < ApplicationResource
  with_id
  attributes :created_at

  with_many :library_books do
    with_id # This DOES NOT work!
    attributes :created_at
  end
end

This doesn't work. Technically, inside of has_many is a separate class which doesn't inherit from the base class (LibraryResource in this example).

helper solves this problem. It's just a mark for methods that should be shared with inline associations.

class ApplicationResource
  include Alba::Resource

  helper do
    def with_id
      attributes(:id)
    end
  end
end
# Now `LibraryResource` works!

Within helper block, all methods should be defined without self..

Experimental: modification API

Alba now provides an experimental API to modify existing resource class without adding new classes. Currently only transform_keys! is implemented.

Modification API returns a new class with given modifications. It's useful when you want lots of resource classes with small changes. See it in action:

class FooResource
  include Alba::Resource

  transform_keys :camel

  attributes :id
end

# Rails app
class FoosController < ApplicationController
  def index
    foos = Foo.where(some: :condition)
    key_transformation_type = params[:key_transformation_type] # Say it's "lower_camel"
    # When params is absent, do not use modification API since it's slower
    resource_class = key_transformation_type ? FooResource.transform_keys!(key_transformation_type) : FooResource
    render json: resource_class.new(foos).serialize # The keys are lower_camel
  end
end

The point is that there's no need to define classes for each key transformation type (dash, camel, lower_camel and snake). This gives even more flexibility.

There are some drawbacks with this approach. For example, it creates an internal, anonymous class when it's called, so there is a performance penalty and debugging difficulty. It's recommended to define classes manually when you don't need high flexibility.

Caching

Currently, Alba doesn't support caching, primarily due to the behavior of ActiveRecord::Relation's cache. See the issue.

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:

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:

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.

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.

Using helper

When we extend AlbaExtension like above, it's not available in inline associations.

class BarResource
  include Alba::Resource
  extend AlbaExtension
  # other attributes
  formatted_time_attributes :created_at, :updated_at

  one :something do
    # This DOES NOT work!
    formatted_time_attributes :updated_at
  end
end

In this case, we can use helper instead of extend.

class BarResource
  include Alba::Resource
  helper AlbaExtension # HERE!
  # other attributes
  formatted_time_attributes :created_at, :updated_at

  one :something do
    # This WORKS!
    formatted_time_attributes :updated_at
  end
end

You can also pass options to your helpers.

module AlbaExtension
  def time_attributes(*attrs, **options)
    attrs.each do |attr|
      attribute(attr, **options) do |object|
        object.__send__(attr).iso8601
      end
    end
  end
end

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:

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.

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.

  1. Logging

Alba currently doesn't support logging directly, but you can add your own logging module to Alba easily.

module Logging
  # `...` was added in Ruby 2.7
  def serialize(...)
    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 signature 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.

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

There are great pioneers in Ruby's ecosystem which does basically the same thing as Alba does. To name a few:

Development

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.

Contributing

Thank you for begin interested in contributing to Alba! Please see contributors guide before start contributing. If you have any questions, please feel free to ask in Discussions.

License

The gem is available as open source under the terms of the MIT License.

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.

alba's People

Contributors

alejandroperea avatar alfonsojimenez avatar danielmalaton avatar davidrunger avatar dependabot-preview[bot] avatar dependabot[bot] avatar estepnv avatar gabrielerbetta avatar geeknees avatar goalaleo avatar heka1024 avatar jaydorsey avatar jgaskins avatar mediafinger avatar naveed-ahmad avatar nilcolor avatar okuramasafumi avatar oshow avatar petergoldstein avatar serhii-sadovskyi avatar shigeyuki-fukuda avatar shmokmt avatar tashirosota avatar trevorturk avatar watson1978 avatar wuarmin avatar yasulab avatar ybiquitous avatar ydah avatar yosiat avatar

Stargazers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

Watchers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar

alba's Issues

Alba and Dependency Injections

Hi,

I would like to use Alba on my app, however I am using dependency injection to play with my controllers (thanks https://dry-rb.org/gems/dry-auto_inject/0.6/ )

This gem assumes that the serializer should know about the resources it has to serialize on the initialization phase.
This breaks the possibility to use dependency injection, and so to use the serializer as an external dependency.

There is a possibility to add a way to add thing like serializer.serialize_ressource(user)

Here is an example of how I use dependency injection on a personal project (we use the same system at work) https://github.com/alex-lairan/GoldivoreBack/blob/master/src/application/controllers/raids/index.rb

It might have forget to add `with:` argument before `:active_support` in the README

I guess it is more of a typing error than a bug report about README.

Describe the bug

When I started rails process, it arose below errors after putting alba.rb file on the config/initializers directory.

/Users/subaru/.rbenv/versions/3.0.2/lib/ruby/gems/3.0.0/gems/alba-2.0.1/lib/alba.rb:57:in `enable_inference!': wrong number of arguments (given 1, expected 0; required keyword: with) (ArgumentError)
        from /Users/subaru/dev/zeroken_api/config/application.rb:29:in `<class:Application>'
        from /Users/subaru/dev/zeroken_api/config/application.rb:26:in `<module:ZerokenApi>'
        from /Users/subaru/dev/zeroken_api/config/application.rb:24:in `<top (required)>'
        from /Users/subaru/.rbenv/versions/3.0.2/lib/ruby/gems/3.0.0/gems/spring-4.0.0/lib/spring/application.rb:93:in `require'
        from /Users/subaru/.rbenv/versions/3.0.2/lib/ruby/gems/3.0.0/gems/spring-4.0.0/lib/spring/application.rb:93:in `preload'
        from /Users/subaru/.rbenv/versions/3.0.2/lib/ruby/gems/3.0.0/gems/spring-4.0.0/lib/spring/application.rb:162:in `serve'
        from /Users/subaru/.rbenv/versions/3.0.2/lib/ruby/gems/3.0.0/gems/spring-4.0.0/lib/spring/application.rb:144:in `block in run'
        from /Users/subaru/.rbenv/versions/3.0.2/lib/ruby/gems/3.0.0/gems/spring-4.0.0/lib/spring/application.rb:138:in `loop'
        from /Users/subaru/.rbenv/versions/3.0.2/lib/ruby/gems/3.0.0/gems/spring-4.0.0/lib/spring/application.rb:138:in `run'
        from /Users/subaru/.rbenv/versions/3.0.2/lib/ruby/gems/3.0.0/gems/spring-4.0.0/lib/spring/application/boot.rb:19:in `<top (required)>'
        from <internal:/Users/subaru/.rbenv/versions/3.0.2/lib/ruby/3.0.0/rubygems/core_ext/kernel_require.rb>:85:in `require'
        from <internal:/Users/subaru/.rbenv/versions/3.0.2/lib/ruby/3.0.0/rubygems/core_ext/kernel_require.rb>:85:in `require'
        from -e:1:in `<main>'

I guess the README described at here might have forget adding with: argument before :active_support.

To Reproduce

  1. create a file which is named 'alba.rb' following this guideline under config/initializers
Alba.backend = :active_support
Alba.enable_inference!(:active_support)
  1. command anything to start rails process such as rails c

Expected behavior

Rails processes will be started successfully.

% rails c
Running via Spring preloader in process 56709
Loading development environment (Rails 6.1.6.1)
irb(main):001:0> 

Actual behavior

It arises errors described above.

Environment

  • Ruby version: 3.0.2
  • Rails version: 6.1.6

Let `to_json` receive an optional argument to adjust it to the implicit call of `to_json` in Rails render method

Is your feature request related to a problem? Please describe.

When I simply pass Alba resource object to render method in Rails6,

render json: FooResource.new(foo_instance)

But this raises ArgumentError coz Rails implicit call of to_json pass an argument(options) while to_json of Alba does NOT.

https://github.com/rails/rails/blob/main/actionpack/lib/action_controller/metal/renderers.rb#L156

Describe the solution you'd like

Let to_json receive an optional argument.

Stack level too deep

Describe the bug

Getting "Stack level too deep" if I add nested relationship.

To Reproduce

class AccountSerializer
      include Alba::Resource
      attributes :id, :role
      one :user, resource: UserSerializer
  end
class UserSerializer
      include Alba::Resource
      attributes :id, :email,:created_at
      many :accounts, resource: AccountSerializer
end

controller:
render json: AccountSerializer.new(accounts)

Expected behavior

To just render one level deep relationship and exit. Ideally we should be able how deep it can go like AMS allows.

Actual behavior

I guess it tries to re-render everything one inside another.

Environment

  • Ruby version: 2.7.6

Additional context

Rails 7

Different style of collection serialization

Alba currently serializes a collection into an Array:

class User
  attr_reader :id

  def initialize(id)
    @id = id
  end
end

user1 = User.new(1)
user2 = User.new(2)

class UserResource
  include Alba::Resource
  attributes :id
end

UserResource.new([user1, user2]).serializable_hash
# => [{:id=>1}, {:id=>2}]

However, sometimes we want to serialize a collection into a Hash with a certain key. The interface will be like:

class UserResource
  include Alba::Resource

  collection_key :id

  attributes :id
end

With collection_key, we can specify the key for the Hash. Now UserResource will serialize a collection differently.

UserResource.new([user1, user2]).serializable_hash
# => {'1' => {:id=>1}, '2' =>  {:id=>2}}

This way, Alba provides more flexible way to deal with collections.

Conditional Attribute should not call the block on failed conditions

For example,

attribute :id
attribute :name, if: proc { |object, _attr| object.respond_to?(:first_name) } do |object|
  object.first_name
end

Here, if we the conditionals fail it still executes the block and causes unexpected behavior

"#<NoMethodError: undefined method `last_name' 

the only workaround is to repeat the conditional check within the block :(

Validation with type and auto-conversion

Is your feature request related to a problem? Please describe.

We sometimes want to validate input data with types. We also want to convert data with the specified type if types mismatch.

Describe the solution you'd like

class UserResource
  include Alba::Resource

  attributes :name, id: [String, true], admin: :Boolean, created_at: [String, ->(object) { object.strftime('%F')}]
end

In the code above, attributes takes arbitrary number of Symbols and Hashes. If Hash argument is included, it's interpreted as "attributes with types".
Hash argument's key is the same as normal attributes, meaning that it's the method name to call on the underlying object. The value part is either Class , Symbol representing JSON native type or Array. When the value is an Array, it's first item is treated as a type and second item is treated as a "auto converter".
"auto converter" is either one of the following.

  • true means it uses default converter. For example, when the type is String, it calls to_s on the attribute
  • false means it doesn't convert the attribute automatically (default)
  • Proc means it uses custom converter represented as a Proc

Question about render :json

Hi! I have another question related with render :json in rails controller.

Is there a way to automatically serialize with alba using render? If not, what is the recommended way to use alba in a rails controller.

Thank you!

`root_key_for_collection` DSL

Is your feature request related to a problem? Please describe.

When setting a root key only for collection, it looks like this:

root_key :_, :foos

Here, the first :_ looks ugly but required.

Describe the solution you'd like

root_key_for_collection :foos

Describe alternatives you've considered

None.

Additional context

This is the real problem in my client work :)

Dealing with circular associations

Consider this case.

  class AuthorResource
    include Alba::Resource
    attributes :id, :first_name, :last_name
    many :books, resource: 'BookResource'
  end

  class GenreResource
    include Alba::Resource
    attributes :id, :title, :description
    many :books, resource: 'BookResource'
  end

  class BookResource
    include Alba::Resource
    attributes :id, :title, :description, :published_at
    many :authors, resource: 'AuthorResource'
    one :genre, resource: 'GerneResource'
  end

Notice that associations is circular. Under current implementation, this causes SystemStackError due to infinite loop.

We want this to be valid usecase. For example, "a book's authors' books" is meaningful. In contrast, however, "a book's authors' books' authors' books" usually doesn't make sense.
That said, when the same resource appears more than three times, it tends to be written in different, better ways.

I'd like to introduce "same resource nest level" to Alba. The default is 2 but we can configure this value. If the same resource appears more than this config value, Alba warns and skips later resource rendering.

Alba.same_resource_nest_level # => 2

class Foo
  attr_accessor :id, :bar
end

class Bar
  attr_accessor :foo
end

class FooResource
  include Alba::Resource
  attributes :id
  one :bar, resource: 'BarResource'
end

class BarResource
  include Alba::Resource
  one :foo, resource: 'FooResource'
end

foo = Foo.new
foo.id = 1
bar = Bar.new
foo.bar = bar
bar.foo = foo

FooResource.new(foo).serialize # => '{"id":1,"bar":{"foo":{"id":1,"bar":{"foo":null}}}}'

Layout feature

Discussed in the discussions board, now I believe supporting layout is something we'd like to have with Alba. The user code would look like:

class UserResource
  include Alba::Resource

  layout file: 'layout.json.erb'

  # ...
end

And the layout file would be a JSON text file with erb:

{
"header": "my_header",
<%= resource.serialized_json %>
}

where resource would be Alba::Resource object.

Discussed in #142

Originally posted by toncid July 1, 2021
Hello,

Great gem you have here! 👍

I was wondering if there is a way to support simple "layouts" in a way that we would have a predefined JSON template with a payload placeholder within it?

The template could look like this:

{
  status: {
    code: 200,
    message: "OK"
  },

  // payload goes here

  pagination: {
    nextPage: "..."
  }
}

The payload would then be yielded from a resource (properly inferred), like:

{
  // status

  user: {...}

  // pagination
}

Or:

{
  // status

  payments: [{...}]

  // pagination
}

Or even come with multiple subfields:

{
  // status

  user: {...},
  payments: [{...}]

  // pagination
}

I haven't found a way to accomplish this with the current gem version (v1.4.0).

Be able to plugin a custom inflector

Hey,

great gem!

Is your feature request related to a problem? Please describe.

If you need key transformation, but there's no active_support around, you have to create workarounds to achieve key-transformation. It would be great, if we could add the possibility to plugin a custom inflector. For example, I have a "hanami-api-dry-project" and there the dry-inflector is around. And I don't want to add the active_support-dependency.

Describe the solution you'd like

One solution might be, to be able to plugin a custom inflector, which has to provide the supported api (#camelize, #dasherize):

Alba.inflector = Dry::Inflector.new # the default one should stay ActiveSupport::Inflector

Are you interested in this feature?

Best regards and thank you

Cascade key transformation seems not work for custom attribute

Describe the bug

To Reproduce

class ProjectResource
  include Alba::Resource
  transform_keys :lower_camel

  root_key :project

  attributes :id, :created_at
  attribute :project_info do |resource|
    {
      created_at: resource.created_at,
      project_name: "name"
    }
  end
end

Expected behavior

{
  "id": 191,
  "createdAt": "2022-10-17 17:42:08",
  "projectInfo": {
  "createdAt": "2022-10-17 17:42:08",
  "projectName": "name"
  }
}

Actual behavior

{
  "id": 191,
  "createdAt": "2022-10-17 17:42:08",
  "projectInfo": {
  "created_at": "2022-10-17 17:42:08",
  "project_name": "name"
  }
}

Environment

  • Ruby version: 3.1.2

Dynamic filter

Hi! Thanks for the project.

I would like to ask if there is a way to include/exclude attributes via params, for example something like this:

UserResource.new(user, exclude: {[:id]}),

should return the json without the "ID".

Thank you!

default value when nil

Is your feature request related to a problem?

my previous serializer was converting nil values to an empty string.
I was wondering if Alba had a way to set this behavior ?

Describe the solution you'd like

class FooSerializer
  include Alba::Resource
  empty_strings_for_nil true # an option such as this
  attributes :foo, :bar
end

Describe alternatives you've considered

right now, I think I need to implement each attributes individually to convert, or change the output response to return nil.
But It's hard to tell how clients might behave

class FooSerializer
  include Alba::Resource

  attribute :foo do |obj|
    obj.foo || ""
  end
  attribute :bar do |obj|
    obj.bar || ""
  end
end

Additional context

image

Deep transform_keys for inline associations

Is your feature request related to a problem? Please describe.

I'd like to have an option for transform_keys to work down through inline associations. Currently, you need to repeat transform_keys within each association block.

Additional context

Here's a code example based on the README which demonstrates the issue:

class User
  attr_reader :created_at
  attr_accessor :articles

  def initialize
    @created_at = Time.now
    @articles = []
  end
end

class Article
  attr_accessor :user_id

  def initialize(user_id)
    @user_id = user_id
  end
end

class AnotherUserResource
  include Alba::Resource

  transform_keys :lower_camel

  attributes :created_at

  many :articles do
    # transform_keys :lower_camel
    
    attributes :user_id
  end
end

user = User.new
article = Article.new(1)
user.articles << article

AnotherUserResource.new(user).to_h

Note the commented-out transform_keys macro, which, when un-commented, results in the desired output. Here's an example from the console showing the difference:

# articles.user_id, not transformed into lower_camel
{:createdAt=>2022-07-26 21:18:29.721693 -0500, :articles=>[{:user_id=>1}]}
# articles.userId, transformed if you repeat the `transform_keys` within the inline association
{:createdAt=>2022-07-26 21:18:29.721693 -0500, :articles=>[{:userId=>1}]}

Ideally, I'd like to avoid repeating transform_keys for inline associations, but things do work as expected currently, so this is only a nice-to-have. I'm not sure if it would be convenient in Alba's code to make this possible, but I thought I'd mention it.

Thank you!

Thanks for this gem

@okuramasafumi Hi again! 😄 👋

I just want to say thanks for your work on this gem. We was using the netflix/fast_json_api previously and we need to migrate to a new gem, and the only well-maintained gem that we found was yours. The other are abbandoned, or not stable.

🍻 🤝

[Doc] README should have some more info about filtering attributes

Discussed in #231

Originally posted by jrochkind September 15, 2022
The README section on "conditional attributes" says:

Filtering attributes with overriding convert works well for simple cases.

and

Below is an example for the same effect as filtering attributes section.

But the link on "filtering attributes section" doesn't work, I can't find a "filtering attributes" section of the README. And I can't find any references to "overriding convert" (which perhaps were in that section?)

I am interested in additional options for "filtering attributes" , what are these references? Thanks!

New backend: oj_rails

Oj has modes and many people will want to use rails mode. We should support it directly with oj_rails backend.
Current oj backend will be aliased as oj_strict.
If someone needs to use object or compat mode of oj, we'll consider supporting them too!

Support for `Oj::StringWriter`

Is your feature request related to a problem? Please describe.

Alba can get more performance boost with streaming encoder as panko does.

Describe the solution you'd like

Implement Oj::StringWriter support.

Additional context

I tried but it was hard enough to make it work. The programming style is quite different. If you're confident to implement the feature, please help!

New option: Overwriting behavior

Abstract

Code like

Class FooResource
  attributes :id, :name
  attribute :name do
    'bar'
  end
end

overwrites an attribute name with 'bar' although a user might want to define simple attribute with attributes. Maybe they misspelled it.
If Alba can warn this kind of overwriting, it'll help users to avoid unintentional overwriting.

Considered Interface

Alba.when_overwriting_attribute # => [:warn(default), :raise, :ignore]

Problems

When they overwrite attributes with inheritance:

Class FooResource
  attributes :id, :name
end

class BarResource < FooResource
  attribute :name do
    'bar'
  end
end

This should be fine.
Current implementation of attributes doesn't recognize where it's defined, so this'll be changed.

Where to keep Configurations?

I am going to try this gem. As a suggestion, here it is not clear where to add the configurations. I am currently moving ahead with creation of the initialize. Maybe docs can be improved further to guide this.

alba/README.md

Line 100 in 0c6f24b

Alba.backend = :oj

Rails integration

In a rails project, where should we create *Resource objects?

We are existing users of jsonapi-serializer in our Rails v7 project, and create *Serializer objects in app/serializers. Is that the same for Alba resources?

Also, is this path configurable? Asking this coz what if we wanna use both Alba and jsonapi-serializer in our Rails project.

Why not json:api?

The library looks flexible enough so adding support in my app for json:api with this gem should be trivial, was wondering if there is a deeper reason on why not supporting it, or is simply that you want to keep this gem nice and clean?

Inconsistent behaviour in collection serialization

Hi,

I was trying to serialize a collection and facing errors if attributes are used inside a block. Is this the expected behaviour?

Thanks.

To Reproduce

class User
  attr_reader :id, :created_at, :updated_at
  attr_accessor :articles

  def initialize(id)
    @id = id
    @created_at = Time.now
    @updated_at = Time.now
    @articles = []
  end
end

class Article
  attr_accessor :user_id, :title, :body

  def initialize(user_id, title, body)
    @user_id = user_id
    @title = title
    @body = body
  end
end

class ArticleResource
  include Alba::Resource

  attribute :dummy_title do |resource|
    resource.title
  end
end

class UserResource
  include Alba::Resource

  attributes :id

  many :articles, 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

Expected Behaviour

"{\"id\":1,\"articles\":[{\"dummy_title\":\"Hello World!\"},{\"dummy_title\":\"Super nice\"}]}"

Actual Behaviour

Traceback (most recent call last):
       16: from /Users/ashwine/.rvm/gems/ruby-2.6.1/gems/alba-1.4.0/lib/alba/resource.rb:108:in `block (2 levels) in converter'
       15: from /Users/ashwine/.rvm/gems/ruby-2.6.1/gems/alba-1.4.0/lib/alba/resource.rb:123:in `key_and_attribute_body_from'
       14: from /Users/ashwine/.rvm/gems/ruby-2.6.1/gems/alba-1.4.0/lib/alba/resource.rb:163:in `fetch_attribute'
       13: from /Users/ashwine/.rvm/gems/ruby-2.6.1/gems/alba-1.4.0/lib/alba/resource.rb:172:in `yield_if_within'
       12: from /Users/ashwine/.rvm/gems/ruby-2.6.1/gems/alba-1.4.0/lib/alba/resource.rb:163:in `block in fetch_attribute'
       11: from /Users/ashwine/.rvm/gems/ruby-2.6.1/gems/alba-1.4.0/lib/alba/many.rb:18:in `to_hash'
       10: from /Users/ashwine/.rvm/gems/ruby-2.6.1/gems/alba-1.4.0/lib/alba/resource.rb:66:in `serializable_hash'
        9: from /Users/ashwine/.rvm/gems/ruby-2.6.1/gems/alba-1.4.0/lib/alba/resource.rb:66:in `map'
        8: from /Users/ashwine/.rvm/gems/ruby-2.6.1/gems/alba-1.4.0/lib/alba/resource.rb:107:in `block in converter'
        7: from /Users/ashwine/.rvm/gems/ruby-2.6.1/gems/alba-1.4.0/lib/alba/resource.rb:107:in `map'
        6: from /Users/ashwine/.rvm/gems/ruby-2.6.1/gems/alba-1.4.0/lib/alba/resource.rb:107:in `each'
        5: from /Users/ashwine/.rvm/gems/ruby-2.6.1/gems/alba-1.4.0/lib/alba/resource.rb:108:in `block (2 levels) in converter'
        4: from /Users/ashwine/.rvm/gems/ruby-2.6.1/gems/alba-1.4.0/lib/alba/resource.rb:123:in `key_and_attribute_body_from'
        3: from /Users/ashwine/.rvm/gems/ruby-2.6.1/gems/alba-1.4.0/lib/alba/resource.rb:162:in `fetch_attribute'
        2: from /Users/ashwine/.rvm/gems/ruby-2.6.1/gems/alba-1.4.0/lib/alba/resource.rb:162:in `instance_exec'
        1: from (irb):77:in `block in <class:ArticleResource>'
NoMethodError (undefined method `title' for #<Array:0x00007f8f741e5c70>)

Environment

  • Ruby version: 2.6.1

Question about Resource / Serializer, can you provide some examples ?

Hi,
I can't figure out how to implement the serializer class
I understand the ressource, but how look like the Serializer class ?

Here is what my serializer from AMS

class IssueSerializer < ActiveModel::Serializer  
  attributes :title, :subject, :description, :conversations, :due_at, :assigned_to
  belongs_to :company
end

and in controller
I would like to use with metadata pagination Pagy

 class IssuesController < ApiController
    include Pagy::Backend
    after_action { pagy_headers_merge(@pagy) if @pagy }
    rescue_from Pagy::OverflowError, with: :redirect_to_last_page

    def index
      @pagy, @isssues = Issue.scope(....)
      render json: { issues: @issues, pagination: pagy_metadata( @pagy, url: true )
   end

end

How would you advise me using Alba in this case ?
Thanks

Allow conditionals on attribute

Hi,

thanks for the hard work on the Gem,
I would like to have the attribute accept conditionals to decide if that key could even be rendered.
Conditional Attributes are supported in ActiveModel::Serializer https://github.com/rails-api/active_model_serializers/tree/0-8-stable#attributes

for example, something like

attribute :id
attribute :student_name, unless: proc {|resource| resource.name.nil? }

now the resulting hash would be

  1. if resource has name
{id: 1, student_name: 'Bob'}
  1. if resource does have name
{id: 1}

Support attribute method in resource

Is your feature request related to a problem? Please describe.

Sometimes we'd like to have a method as an attribute. For example, consider this example from README.

class User
  attr_accessor :id, :name, :email, :created_at, :updated_at
  def initialize(id, name, email)
    @id = id
    @name = name
    @email = email
    @created_at = Time.now
    @updated_at = Time.now
  end
end

class UserResource
  include Alba::Resource

  root_key :user

  attributes :id, :name

  attribute :name_with_email do |resource|
    "#{resource.name}: #{resource.email}"
  end
end

user = User.new(1, 'Masafumi OKURA', '[email protected]')
UserResource.new(user).serialize
# => "{\"user\":{\"id\":1,\"name\":\"Masafumi OKURA\",\"name_with_email\":\"Masafumi OKURA: [email protected]\"}}"

Describe the solution you'd like

class UserResource
  include Alba::Resource

  root_key :user

  attributes :id, :name, :name_with_email

  def :name_with_email(resource)
    "#{resource.name}: #{resource.email}"
  end
end

Here we define name_with_email as a method, not a block. This is useful when we want this method to be inherited.

Describe alternatives you've considered

We can leave it unimplemented since we can achieve the same goal with block syntax.

Auto pluck for ActiveRecord

Is your feature request related to a problem? Please describe.

Using pluck for performance is a common technique in Rails, but Alba doesn't support pluck so we must fetch all columns although we might not use it.

Describe the solution you'd like

Alba.auto_pluck = true

class UserResource
  include Alba::Resource

  attributes :id
end

# User has id, name and email
class User < ApplicationRecord
end

UserResource.new(User.all).serialize
# => Now it executes `SELECT id` query instead of `SELECT *`

Cascade key transformation only cascades for one level

Describe the bug

Apologies for not testing this myself sooner, but I think we have a bug left over from #220, where transform_keys only cascades for one level.

To Reproduce

Given the following code sample, note that the articles array has the createdAt key, but the comments array has the created_at key, which has not been transformed:

class User
  attr_accessor :created_at, :articles

  def initialize
    @created_at = Time.now
  end
end

class Article
  attr_accessor :created_at, :comments

  def initialize
    @created_at = Time.now
  end
end

class Comment
  attr_accessor :created_at

  def initialize
    @created_at = Time.now
  end
end

class UserResource
  include Alba::Resource

  transform_keys :lower_camel

  attributes :created_at

  association :articles do
    attributes :created_at

    association :comments do
      attributes :created_at
    end
  end
end

user = User.new
comment = Comment.new
article = Article.new
article.comments = [comment]
user.articles = [article]

UserResource.new(user).to_h
# => 
{"createdAt"=>2022-10-24 15:23:10.190596 -0500,
 "articles"=>[{"createdAt"=>2022-10-24 15:23:10.203395 -0500, "comments"=>[{"created_at"=>2022-10-24 15:23:10.198995 -0500}]}]}

Can't get root_key to work (without inference)

Describe the bug

:root_key does not seem to be doing what I expect, I can't get it to have an effect. I am not using inference, but still expect to be able to specify a root key manually.

To Reproduce

class MyThing
  attr_accessor :name
  def initialize(name:)
    @name = name
  end
end

class TestSerializer
  include Alba::Resource

  root_key :widget

  attributes :name
end

TestSerializer.new( MyThing.new(name: "joe") ).serializable_hash
# => {:name=>"joe"}

Expected behavior

I expected there to be a root key in the output, like

{ widget: {:name=>"joe"} }

Actual behavior

There is no root key in the output, just {:name=>"joe"}. The root_key seems to be ignored?

This also means I can't get meta to output, because it requires a recognized root_key.

Additional context

Alba 1.6.0

I know the root_key function is integrated with "inference", I am not using alba inference, not calling Alba.enable_inference!, not sure if that matters.

Equivalent when I don't know the model/serializer at play?

I am looking for an AMS equivalent to

          instance_serialization = ActiveModelSerializers::SerializableResource.new(local_instance).as_json

basically, I don't know what object local_instance is and what serializer class I should be using. AMS does that for me automatically.

Thank you

Is it ok to add rake command for rails installation ?

Is your feature request related to a problem? Please describe.

Feature request.
I am using Alba with Rails then thought will be happy if we have rake command to install to rails.

Describe the solution you'd like

Add rake command to install alba setting(like initializer), alba resource file template.
Or it could be done by rails generate command.

Inconsistentcy between `#serialize` (aka `#to_json`) and `#serializable_hash` (aka `#as_json`)

Hi! First of all, thank you for putting this project together! 🏆

I'm working on a Rails project, and I stumbled across the fact that #as_json doesn't consider root keys. This stops me me from writing

render json: UserSerializer.new(user), status: :ok

in my controllers. This forces me to render a text/raw payload and take care of the content-type header. More importantly, it breaks the expectation that JSON.generate(obj.as_json) == obj.to_json.

Is this something by design? Or may I attempt to change it?

possible to refactor this attribute to use a symbol ?

works fine, I'd like to write it to be easier to read it

current code

  attribute :overridden_at, if: proc { |resource|
                                  resource.respond_to?(:overridden_at) && resource.overridden_at.present?
                                } do |resource|
    resource.overridden_at.iso8601(3)
  end

Describe the solution you'd like

We need to call respond_to? because the attribute comes from a joined relation, which is not always present when calling this serializer.
then we don't want to include the field in the json when its nil, thus using the if: proc

if we could write it such as

attribute :overridden_at, if: :has_overriden_at? do |resource|
  resource.overridden_at.iso8601(3)
end

def has_overriden_at?(resource)
  resource.respond_to?(:overridden_at) && resource.overridden_at.present?
end

is this already possible?

Automatic serializer class lookup for Rails?

Is your feature request related to a problem? Please describe.

We have following code in a controller of our Rails application:

def index
  nodes = Node.where(index_conditions).map {|node| node.real_node }
  render json: nodes, action: 'index'
end

Node#real_node method is a polymorphic association like this:

class Node < ApplicationController
  belongs_to :real_node, polymorphic: true
end

Because elements of the rendered array dynamically change at runtime, I can't determine serializer class statically.

Describe the solution you'd like

To overcome the problem, I added following initializer file at config/initializers/ in our rails app:

module AlbaSerializerLookup
  CLASS_SERIALIZER_CACHE = {}

  def self.serializer_for(object, serializer: nil)
    if serializer
      if serializer.is_a?(String) || serializer.is_a?(Symbol)
        serializer = ActiveSupport::Inflector.safe_constantize(serializer)
      end
      serializer
    else
      serializer_for_class(object.class)
    end
  end

  def self.serializer_for_class(klass)
    CLASS_SERIALIZER_CACHE[klass] ||= serializer_for_class_without_cache(klass)
  end

  def self.serializer_for_class_without_cache(klass)
    # This returns ArraySerializer if klass is Array
    ActiveSupport::Inflector.safe_constantize(:"#{klass.name}Serializer")
  end
end

class ObjectSerializer
  include Alba::Resource

  def converter
    inner_params = params.dup
    inner_serializer_param = inner_params.delete(:serializer)

    lambda do |object|
      serializer = AlbaSerializerLookup.serializer_for(object, serializer: inner_serializer_param)
      if serializer && serializer != ObjectSerializer
        resource = serializer.new(object, params: inner_params)
        resource.serializable_hash
      else
        object
      end
    end
  end
end

class RootSerializer < ObjectSerializer
  def serializable_hash
    if collection? && !object.is_a?(Hash)
      root_array_serializable_hash
    else
      converter.call(object)
    end
  end

  alias to_h serializable_hash

  private

  def root_array_serializable_hash
    serializable_array = object.map(&converter)

    if object.respond_to?(:total)
      {
        data: serializable_array,
        total: object.total,
      }
    else
      {
        data: serializable_array,
      }
    end
  end
end

ActionController::Renderers.add :json do |object, options|
  resource = RootSerializer.new(object, params: options)
  string = resource.serialize
  send_data string, type: Mime[:json]
end

This approach is basically inspired by following logics of ActiveModelSerializers:

Support for render json: ... syntax is useful when we want to support both HTML and JSON response from the same controller method, although we're not using it at this moment:

def index
  nodes = Node.where(index_conditions).map {|node| node.real_node }
  respond_to do |format|
    format.json {render json: nodes}
    format.html  {render json: nodes}
  end
end

Describe alternatives you've considered

It would be great if Alba gem has out-of-the-box support for json rendering for Rails with dynamic serializer class lookup.

Additional context

Verified code snippets with:

  • rails-7.0.3
  • alba-1.6.0

Meta data support

Is your feature request related to a problem? Please describe.

We need some form of support for meta data alongside the main (serialized) data.

Describe the solution you'd like

class UserResource
  include Alba::Resource

  attributes :id, :name

  meta name: :size do |users|
    users.size
  end
end

UserResource.new([user1, user2], key: :users).serialize
# => '{"users":[{"id":1,"name":"user1"},{"id":2,"name":"user2"}],"size":2}'

Describe alternatives you've considered

In this proposal the return value of meta block is a Hash and it's used directly. I'm not sure if we're happy to use some sort of DSL in meta block to build a Hash.

class UserResource
  include Alba::Resource

  attributes :id, :name

  meta do |users|
    attribute :size { users.size }
  end
end

UserResource.new([user1, user2], key: :users).serialize
# => '{"users":[{"id":1,"name":"user1"},{"id":2,"name":"user2"}],"meta"{"size":2}}'

Add README benchmarks

Is your feature request related to a problem? Please describe.

Would it be possible to include the benchmarks as part of the README so you can showcase more of the CPU and memory performance more easily? I think these benchmarks are super helpful and would be nice to be more accessible for folks.

Describe the solution you'd like

Two ideas some to mind:

  • Add a Benchmarks section to the README and post details.
  • Add a data section to your benchmark source code, like I do with my benchmarks. This way you can keep the stats close to the source code for reference (this might be even better than throwing this in the README in case you want to keep the README shorter in length).

Describe alternatives you've considered

N/A

Additional context

Here's an sample of what I'm seeing when running the collection benchmarks in case it helps (or more for anyone reading this that might be curious):

Collection Benchmarks
Warming up --------------------------------------
                alba    14.000  i/100ms
         alba_inline    22.000  i/100ms
                 ams     1.000  i/100ms
         blueprinter    11.000  i/100ms
     fast_serializer    12.000  i/100ms
            jbuilder    18.000  i/100ms
         jserializer    20.000  i/100ms
             jsonapi    11.000  i/100ms
 jsonapi_same_format    11.000  i/100ms
               panko    58.000  i/100ms
           primalize    14.000  i/100ms
               rails    21.000  i/100ms
       representable     7.000  i/100ms
          simple_ams     5.000  i/100ms
       turbostreamer    21.000  i/100ms
Calculating -------------------------------------
                alba    150.412  (±49.9%) i/s -    588.000  in   5.186892s
         alba_inline    171.853  (±41.9%) i/s -    682.000  in   5.224603s
                 ams     19.455  (± 5.1%) i/s -     97.000  in   5.012393s
         blueprinter     86.935  (±43.7%) i/s -    330.000  in   5.023697s
     fast_serializer    110.387  (±22.6%) i/s -    516.000  in   5.092511s
            jbuilder    139.415  (±43.0%) i/s -    504.000  in   5.156183s
         jserializer    178.616  (±26.3%) i/s -    820.000  in   5.163328s
             jsonapi     85.341  (±43.4%) i/s -    297.000  in   5.033005s
 jsonapi_same_format     88.903  (±40.5%) i/s -    330.000  in   5.215683s
               panko    574.515  (± 7.7%) i/s -      2.900k in   5.088445s
           primalize    116.128  (±54.3%) i/s -    350.000  in   5.157945s
               rails    182.941  (±30.6%) i/s -    819.000  in   5.335402s
       representable     65.774  (±28.9%) i/s -    287.000  in   5.135179s
          simple_ams     45.796  (±43.7%) i/s -    145.000  in   5.423215s
       turbostreamer    179.497  (±27.9%) i/s -    798.000  in   5.157985s

Comparison:
               panko:      574.5 i/s
               rails:      182.9 i/s - 3.14x  (± 0.00) slower
       turbostreamer:      179.5 i/s - 3.20x  (± 0.00) slower
         jserializer:      178.6 i/s - 3.22x  (± 0.00) slower
         alba_inline:      171.9 i/s - 3.34x  (± 0.00) slower
                alba:      150.4 i/s - 3.82x  (± 0.00) slower
            jbuilder:      139.4 i/s - 4.12x  (± 0.00) slower
           primalize:      116.1 i/s - 4.95x  (± 0.00) slower
     fast_serializer:      110.4 i/s - 5.20x  (± 0.00) slower
 jsonapi_same_format:       88.9 i/s - 6.46x  (± 0.00) slower
         blueprinter:       86.9 i/s - 6.61x  (± 0.00) slower
             jsonapi:       85.3 i/s - 6.73x  (± 0.00) slower
       representable:       65.8 i/s - 8.73x  (± 0.00) slower
          simple_ams:       45.8 i/s - 12.55x  (± 0.00) slower
                 ams:       19.5 i/s - 29.53x  (± 0.00) slower

Calculating -------------------------------------
                alba   971.369k memsize (     0.000  retained)
                        13.019k objects (     0.000  retained)
                        12.000  strings (     0.000  retained)
         alba_inline   985.137k memsize (    11.008k retained)
                        13.077k objects (     8.000  retained)
                        12.000  strings (     0.000  retained)
                 ams     4.472M memsize (   168.000  retained)
                        60.544k objects (     1.000  retained)
                        14.000  strings (     0.000  retained)
         blueprinter     1.589M memsize (     0.000  retained)
                        22.105k objects (     0.000  retained)
                         2.000  strings (     0.000  retained)
     fast_serializer     1.232M memsize (     0.000  retained)
                        13.908k objects (     0.000  retained)
                         3.000  strings (     0.000  retained)
            jbuilder     1.774M memsize (     0.000  retained)
                        21.010k objects (     0.000  retained)
                         7.000  strings (     0.000  retained)
         jserializer   819.705k memsize (    16.800k retained)
                        10.109k objects (   100.000  retained)
                         2.000  strings (     0.000  retained)
             jsonapi     2.280M memsize (     0.000  retained)
                        32.415k objects (     0.000  retained)
                        50.000  strings (     0.000  retained)
 jsonapi_same_format     2.132M memsize (     0.000  retained)
                        30.314k objects (     0.000  retained)
                        50.000  strings (     0.000  retained)
               panko   230.418k memsize (     0.000  retained)
                         3.031k objects (     0.000  retained)
                         2.000  strings (     0.000  retained)
           primalize     1.195M memsize (     0.000  retained)
                        20.015k objects (     0.000  retained)
                         4.000  strings (     0.000  retained)
               rails     1.237M memsize (     0.000  retained)
                        13.304k objects (     0.000  retained)
                         3.000  strings (     0.000  retained)
       representable     2.869M memsize (     0.000  retained)
                        31.024k objects (     0.000  retained)
                        50.000  strings (     0.000  retained)
          simple_ams     7.901M memsize (     0.000  retained)
                        68.721k objects (     0.000  retained)
                         5.000  strings (     0.000  retained)
       turbostreamer   780.880k memsize (     0.000  retained)
                        12.144k objects (     0.000  retained)
                        33.000  strings (     0.000  retained)

Comparison:
               panko:     230418 allocated
       turbostreamer:     780880 allocated - 3.39x more
         jserializer:     819705 allocated - 3.56x more
                alba:     971369 allocated - 4.22x more
         alba_inline:     985137 allocated - 4.28x more
           primalize:    1195163 allocated - 5.19x more
     fast_serializer:    1232385 allocated - 5.35x more
               rails:    1236761 allocated - 5.37x more
         blueprinter:    1588937 allocated - 6.90x more
            jbuilder:    1774157 allocated - 7.70x more
 jsonapi_same_format:    2132489 allocated - 9.25x more
             jsonapi:    2279958 allocated - 9.89x more
       representable:    2869126 allocated - 12.45x more
                 ams:    4471529 allocated - 19.41x more
          simple_ams:    7900985 allocated - 34.29x more

Feature Request: Case options

Summary

When we use Rails as an API, we want to return JSON, not HTML.
In that case, we may want to make it a camel case.
It would be nice to have an option to handle those situations.

Example

In the case of gem 'jsonapi-serializer', we can specify the cases as follows.

class UserSerializer
  include JSONAPI::Serializer

  attribute :user do |object|
    {
      last_name: object.last_name,
      first_name: object.first_name,
      phone_number: object.phone_number,
      zip: object.zip,
      prefecture: object.prefecture,
      address: object.address
    }
  end

  set_key_transform :camel_lower
end

Expected DSL

class UserSerializer
  include Alba::Resource

  attribute :user do |object|
    {
      last_name: object.last_name,
      first_name: object.first_name,
      phone_number: object.phone_number,
      zip: object.zip,
      prefecture: object.prefecture,
      address: object.address
    }
  end
  
  set_key_transform :camel_lower
end

Want `resource:` and `each_resources:` option to Alba.serialize like ActiveModelSerializers `serializer:` and `each_serializers:`

Is your feature request related to a problem? Please describe.

Model has many serializers in many cases.I want to select resources class.

# ex. ActiveModelSerializers
ActiveModelSerializers::SerializableResource.new(
  serializer: CustomSerializer, # This
).serializable_hash

Describe the solution you'd like

Alba.serialize(object, resource: CustomResource, root_key: :object)
Alba.serialize(objects, each_resources: CustomResource, root_key: :object)

Change/Override params in associations

Would it be possible to allow something like this?

class FooJson
  include Alba::Resource
  one :bar, resource: BarJson, params: { some: "other_value" }
end

FooJson.new(data, params: { some: "value" }).serialize

Thanks!

Compatibility with zeitwerk

Describe the bug

When I write certain code, I get an error in zeitwerk:check.

$ rails zeitwerk:check                                                        
Hold on, I am eager loading the application.
rails aborted!
NameError: uninitialized constant CommentResource::CommentParentResource

  one :parent, resource: CommentParentResource
                         ^^^^^^^^^^^^^^^^^^^^^
/alba-zeitwerk-issue/app/resources/comment_resource.rb:7:in `<class:CommentResource>'
/alba-zeitwerk-issue/app/resources/comment_resource.rb:1:in `<main>'
/alba-zeitwerk-issue/app/resources/comment_parent_resource.rb:1:in `<main>'
Tasks: TOP => zeitwerk:check

To Reproduce

reproduction code
https://github.com/ima2251/alba-zeitwerk-issue

Expected behavior

No error in zeitwerk:check.

Actual behavior

Environment

  • Please check my repository.

Additional context

Random error when deploying to Heroku.Sometimes it deploys successfully, sometimes it fails.
The cause of the error is the same as with zeritwerk:check.

Normal code works fine, so we won't notice it during normal development, unless we do a zeitwerk:check.

Logging

It's a good idea to provide logging facility for performance monitoring or debugging.
It might be even better to provide around hook for some methods so that users can do something (logging in this context but various things can be done) before and after the targeted methods.

Recommend Projects

  • React photo React

    A declarative, efficient, and flexible JavaScript library for building user interfaces.

  • Vue.js photo Vue.js

    🖖 Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.

  • Typescript photo Typescript

    TypeScript is a superset of JavaScript that compiles to clean JavaScript output.

  • TensorFlow photo TensorFlow

    An Open Source Machine Learning Framework for Everyone

  • Django photo Django

    The Web framework for perfectionists with deadlines.

  • D3 photo D3

    Bring data to life with SVG, Canvas and HTML. 📊📈🎉

Recommend Topics

  • javascript

    JavaScript (JS) is a lightweight interpreted programming language with first-class functions.

  • web

    Some thing interesting about web. New door for the world.

  • server

    A server is a program made to process requests and deliver data to clients.

  • Machine learning

    Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.

  • Game

    Some thing interesting about game, make everyone happy.

Recommend Org

  • Facebook photo Facebook

    We are working to build community through open source technology. NB: members must have two-factor auth.

  • Microsoft photo Microsoft

    Open source projects and samples from Microsoft.

  • Google photo Google

    Google ❤️ Open Source for everyone.

  • D3 photo D3

    Data-Driven Documents codes.