adequate_serialization
adequate_serialization copied to clipboard
Serializes objects adequately
AdequateSerialization
AdequateSerialization allows you to define serializers that will convert your objects into simple hashes that are suitable for variable purposes such as caching or using in an HTTP response. It stems from the simple idea of giving slightly more control over the as_json method that gets called when objects are serialized using Rails' default controller serialization.
- Installation
- Usage
- Defining attributes
- :if
- :unless
- :optional
- Attaching objects
- Usage with Rails
- Cache busting
- Caching plain objects
- Advanced
- Defining attributes
- Development
- Contributing
- License
Installation
Add this line to your application's Gemfile:
gem 'adequate_serialization'
And then execute:
$ bundle
Or install it yourself as:
$ gem install adequate_serialization
Usage
There are two ways to define the serialization process for objects.
For larger objects where it makes sense to define the serialization in a separate class, you should include the AdequateSerialization::Serializable in the object that you want to be able to serialize. Then, define a serializer matching the name of that object, postfixed with "Serializer", as in:
class UserSerializer < AdequateSerialization::Serializer
attribute :id, :name, :title
end
For smaller objects where it makes sense to define the serialization inline, you can include the result of the AdequateSerialization::inline method, as in:
class User
include AdequateSerialization.inline { attribute :id, :name, :title }
...
end
For both types of serialization definition, you can then use the AdequateSerialization DSL to define the attributes that are available to the serializer. You can then call as_json on any instance of that object to get the resultant hash. Below is an example:
User.new(id: 1, name: 'Clark Kent', title: 'Superman').as_json
# => {:id=>1, :name=>"Clark Kent", :title=>"Superman"}
Defining attributes
The AdequateSerialization::Serializer DSL is just the one attribute method. You can pass as many names as you want, and each attribute will become a key in the resultant serialized hash. If you need to build a "synthesized" attribute (one that is defined in the serializer), you can do so with a block that receives the object as an argument, as in:
class UserSerializer < AdequateSerialization::Serializer
attribute :double_name do |user|
user.name * 2
end
end
There are also a couple of options that you can pass to the attribute method as the last argument that modify the serializer's behavior, listed below.
:if
If you pass an :if condition, that method will be called on the serializable object to determine whether or not that attribute should be included in the resultant hash, as in:
class UserSerializer < AdequateSerialization::Serializer
attribute :title, if: :manager?
end
user = User.new(...)
user.as_json
# => {:id=>1, :name=>"Clark Kent"}
user.update(manager: true)
user.as_json
# => {:id=>1, :name=>"Clark Kent", :title=>"Superman"}
:unless
This is the same as the :if option, but will result in the opposite behavior (the attribute will be present if the predicate is not met).
:optional
There are times when you want to include an attribute that you normally wouldn't. For example, if you have both Post and Comment objects, normally you wouldn't include the post attribute on the child comment objects. However, if you're serializing just the comment, it might be useful to have the post attached. In this case, you could mark the attribute as optional and it would only be included if it was listed in the :includes option passed to the as_json method, as in:
class PostSerializer < AdequateSerialization::Serializer
attribute :id, :title, :body
attribute :comments, optional: true
end
class CommentSerializer < AdequateSerialization::Serializer
attribute :id, :body
attribute :post, optional: true
end
comment = Comment.new(...)
comment.as_json
# => {:id=>1, :body=>"This is a great gem!"}
comment.as_json(includes: :post)
# => {:id=>1, :body=>"This is a great gem!", :post=>{:id=>1, :title=>"Introducing Adequate Serializer", :body=>"This is adequate serializer."}}
The includes key can take either a single name or an array of names.
Attaching objects
There are times where it's more performant to serialize the objects using normal serialization and to attach an additional attribute later. For instance, you could serialize all of the posts and then attach whether or not a user had upvoted them. In that case, there's a special syntax that looks like the below:
upvotes =
User.upvotes.each_with_object({}) do |post, votes|
votes[post.id] = true
end
# => {1=>true}
Post.all.map(&:as_json)
# => [{:id=>1}, {:id=>2}]
posts = Post.all.map { |post| post.as_json(attach: { upvoted: upvotes }) }
# => [{:id=>1, :upvoted=>true}, {:id=>2, :upvoted=>false}]
This relies on the objects to which you are attaching having an id attribute and the attachable hash being an index of id pointing to the attribute value.
Usage with Rails
If ::Rails is defined when adequate_serialization is required, it will hook into ActiveRecord in three ways:
- By including
AdequateSerializer::SerializableinActiveRecord::Baseso that all of your models will be serializable by overwritingActiveRecord::Base'sas_jsonmethod, which by default will useRails.cache.fetch. - By overwriting
ActiveRecord::Relation'sas_jsonmethod to use theAdequateSerializer::Rails::RelationSerializerobject, which by default will use theRails.cache.fetch_multimethod in order to more efficiently serialize all of the records in the relation. - By introducing cache busting behavior in the background using
ActiveJobif you're serializing objects outside of a one-to-many relationship.
Cache busting
When using adequate_serialization with rails, each attribute call will check if you're serializing an association. If you are, then it will ensure you have appropriate caching behavior enabled:
- If it's a
has_manyorhas_oneassociation, then it will make sure that the inverse has thetouch: trueoption on the association. - If it's a
belongs_toassociation, then it will add anafter_update_commithook to the inverse class that will loop through the associated objects and bust the association using anActiveJobtask.
You can visualize this cache busting behavior with a prebaked Rack application that is shipped with this gem by adding the following to your config/routes.rb file:
if Rails.env.development?
mount AdequateSerialization::Rails::CacheVisualization,
at: '/cache_visualization'
end
This will allow you to view which caches will bust which others in development by navigating to your application's /cache_visualization path.
Caching plain objects
You can still use plain objects to be serialized, and if you want to take advantage of the caching behavior, you can define a cache_key method on the objects that you're serializing. This will cause AdequateSerialization to start putting them into the Rails cache.
The result is that you can now this in your controllers:
class UsersController
def show
user = User.find(params[:id])
render json: { user: user }
end
end
and the response will be the serialized user. You can pass additional options that will get forwarded on to the serializer as well, as in:
class UsersController
def show
user = User.find(params[:id])
render json: { user: user }, includes: :title
end
end
and the result will now contain the title attribute (provided it was configured as an optional attribute). All options that previously were passed in to the as_json method get forwarded appropriately.
Advanced
The serialization process happens through a series of AdequateSerialization::Steps. The caching behavior mentioned in the Usage with Rails section is one such step that gets introduced. You can introduce more yourself like so:
class LoggingStep < AdequateSerialization::Steps::Step
def apply(response)
Logger.log("#{response.object} is being serialized with #{response.opts} options")
apply_next(response)
end
end
AdequateSerialization.prepend(LoggingStep)
This will cause this object to be placed into the list of steps taken to serialize objects, and can be used for much more powerful and advanced workflows.
Development
After checking out the repo, run bin/setup to install dependencies. Then, run rake test to run the tests. You can also run bin/console for an interactive prompt that will allow you to experiment.
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
Bug reports and pull requests are welcome on GitHub at https://github.com/CultureHQ/adequate_serialization.
License
The gem is available as open source under the terms of the MIT License.