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.
Add this line to your application's Gemfile:
gem "halitosis"And then execute:
$ bundle installOr install it yourself as:
$ gem install halitosisCreate 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
endInstantiate:
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"}}}}'Not associated with any particular resource or collection. For example, an API entry point:
class ApiRootSerializer
include Halitosis
link(:self) { '/api' }
endRepresents a single item:
class DuckSerializer
include Halitosis
resource :duck
endWhen 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 defaultRepresents 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
endThe block should return an array of Halitosis instances in order to be rendered.
Attributes can be defined in several ways:
attribute(:quacks) { "#{duck.quacks} per minute" }attribute :quacks # => Duck#quacks, if resource is declaredattribute :quacks, value: "many"attribute :quacks do
duck.quacks.round
endattribute(:quacks) { calculate_quacks }
def calculate_quacks
...
endAttributes can also be implemented using the legacy property alias:
property(:quacks) { "#{duck.quacks} per minute" }
property :quacks # Duck#quacks
property :quacks, value: "many"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
endWith 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
[ ... ]
endSimple 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] # nilSimple 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)
endA rel shorthand is also available for those who like to avoid a relationship:
rel(:parent) { UserSerializer.new(...) }
rel(:ducklings) { [DucklingSerializer.new(...), ...] }
endResources 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"])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 }}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 }}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)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.
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.
The gem is available as open source under the terms of the MIT License.
Everyone interacting in the Halitosis project's codebases, issue trackers, chat rooms and mailing lists is expected to follow the code of conduct.