Project

halitosis

0.0
The project is in a healthy, maintained state
Provides an interface for serializing resources as JSON with HAL-like links and relationships.
2005
2006
2007
2008
2009
2010
2011
2012
2013
2014
2015
2016
2017
2018
2019
2020
2021
2022
2023
2024
 Dependencies
 Project Readme

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:

  1. The proc returns either a Halitosis instance or an array of Halitosis instances
  2. 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.