Halitosis
bmorrall: I’ve come up with the best name for a rails library!!!
bmorrall: HAL is an API design standard. I like what it does, but it doesn’t fully mesh well with rails.
bmorrall: So I’m thinking of adapting the standard to work better with rails, and bundling up a library to help generate the required data from it
bmorrall: Calling it "rails_is_hal"
daveabbott: Or Halitosis.
bmorrall: That’s also not a bad idea, and slightly more professional sounding
Provides an interface for serializing resources as JSON with HAL-like links and relationships, with additonal meta and permissions info.
Need something more standardized (JSON:API, or HAL)? Most of this code was converted from halogen; which is a great alternative for HAL+JSON serialization.
Installation
Add this line to your application's Gemfile:
gem "halitosis"
And then execute:
$ bundle install
Or install it yourself as:
$ gem install halitosis
Basic usage
Create a simple serializer class and include Halitosis:
class Duck
def name = "Ferdi"
def code = "ferdi"
end
class DuckSerializer
include Halitosis
resource :duck
attribute :name
link :self do
"/ducks/#{duck.code}"
end
end
Instantiate:
duck = Duck.new
serializer = DuckSerializer.new(duck)
Then call serializer.render
:
{
duck: {
name: 'Ferdi',
_links: {
self: { href: '/ducks/ferdi' }
}
}
}
Or serializer.to_json
:
'{"duck": {"name": "Ferdi", "_links": {"self": {"href": "/ducks/ferdi"}}}}'
Serializer types
1. Simple
Not associated with any particular resource or collection. For example, an API entry point:
class ApiRootSerializer
include Halitosis
link(:self) { '/api' }
end
2. Resource
Represents a single item:
class DuckSerializer
include Halitosis
resource :duck
end
When a resource is declared, #initialize
expects the resource as the first argument:
serializer = DuckSerializer.new(Duck.new, ...)
This makes attribute definitions cleaner:
attribute :name # now calls Duck#name by default
3. Collection
Represents a collection of items. When a collection is declared, #initialize
expects the collection as the first argument:
class DuckKidsSerializer
include Halitosis
collection :ducklings do
[ ... ]
end
end
The block should return an array of Halitosis instances in order to be rendered.
Defining attributes, links, relationships, meta, and permissions
Attributes can be defined in several ways:
attribute(:quacks) { "#{duck.quacks} per minute" }
attribute :quacks # => Duck#quacks, if resource is declared
attribute :quacks, value: "many"
attribute :quacks do
duck.quacks.round
end
attribute(:quacks) { calculate_quacks }
def calculate_quacks
...
end
Attributes can also be implemented using the legacy property
alias:
property(:quacks) { "#{duck.quacks} per minute" }
property :quacks # Duck#quacks
property :quacks, value: "many"
Conditionals
The inclusion of attributes can be determined by conditionals using if
and
unless
options. For example, with a method name:
attribute :quacks, if: :include_quacks?
def include_quacks?
duck.quacks < 10
end
With a proc:
attribute :quacks, unless: proc { duck.quacks.nil? }, value: ...
For links and relationships:
link :ducklings, :templated, unless: :exclude_ducklings_link?, value: ...
relationship :ducklings, if: proc { duck.ducklings.size > 0 } do
[ ... ]
end
Links
Simple link:
link(:root) { '/' }
# => { _links: { root: { href: '/' } } ... }
Templated link:
link(:find, :templated) { '/ducks/{?id}' }
# => { _links: { find: { href: '/ducks/{?id}', templated: true } } ... }
Optional links:
serializer = MySerializerWithManyLinks.new(include_links: false)
rendered = serializer.render
rendered[:_links] # nil
Relationships
Simple one-to-one relationship:
relationship(:owner) { UserSerializer.new(duck.owner) }
# => { duck: { _relationships: { owner: { ... } } } }
or a one-to-many collection with an array of record serializers:
relationship(:ducklings) do
duck.ducklings.map { |duckling| DucklingSerializer.new(duckling) }
end
# => { duck: { _relationships: { ducklings: [ ... ] } } }
or with a single collection serializer:
relationship(:ducklings) do
DucklingsSerializer.new(duck.ducklings)
end
A rel shorthand is also available for those who like to avoid a relationship:
rel(:parent) { UserSerializer.new(...) }
rel(:ducklings) { [DucklingSerializer.new(...), ...] }
end
Resources are not rendered by default. They will be included if both of the following conditions are met:
- The proc returns either a Halitosis instance or an array of Halitosis instances
- The relationship is requested via the parent serializer's options, e.g.:
DuckSerializer.new(include: { ducklings: true, parent: false })
They can also be prested as an array of strings:
DuckSerializer.new(include: ["ducklings", "parent"])
or as comma-joined strings:
DuckSerializer.new(include: "ducklings,parent")
Resources can be nested to any depth, e.g.:
DuckSerializer.new(include: {
ducklings: {
foods: {
ingredients: true
},
pond: true
}
})
or:
DuckSerializer.new(include: "ducklings.foods.ingredients,ducklings.pond")
and requested on collections:
DucksSerializer.new(..., include: ["ducks.ducklings.foods"])
Meta
Simple nested Meta information. Use this for providing details of attributes that are not modified directly by the API.
meta(:created_at)
# => { _meta: { created_at: "2024-09-30T20:46:00Z }}
Permissions
Simple nested Access Rights information. Use this for informing clients of what resources they are able to access.
permission(:snuggle) -> { duckling_policy.snuggle? }
# => { _permissions: { snuggle: true }}
Using with Rails
If Halitosis is loaded in a Rails application, Rails url helpers will be available in serializers:
link(:new) { new_duck_url }
Serializers can either be passed in as a json argument to render:
render json: DuckSerializer.new(duck)
or directly given as arguments to render:
render DuckSerializer.new(duck)
Development
After checking out the repo, run bin/setup
to install dependencies. Then, run rake spec
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 the created tag, and push the .gem
file to rubygems.org.
Contributing
Bug reports and pull requests are welcome on GitHub at https://github.com/bmorrall/halitosis. This project is intended to be a safe, welcoming space for collaboration, and contributors are expected to adhere to the code of conduct.
License
The gem is available as open source under the terms of the MIT License.
Code of Conduct
Everyone interacting in the Halitosis project's codebases, issue trackers, chat rooms and mailing lists is expected to follow the code of conduct.