Yaks
Ruby library for building hypermedia APIs
Install / Use
/learn @plexus/YaksREADME
(https://badges.gitter.im/Join Chat.svg)
Yaks
<img align="left" src="https://raw.githubusercontent.com/plexus/yaks/master/graphics/logo_small.png">The library that understands hypermedia.
If you use Yaks please help out by filling out the Yaks Users Survey
Yaks takes your data and transforms it into hypermedia formats such as HAL, JSON-API, or HTML. It allows you to build APIs that are discoverable and browsable. It is built from the ground up around linked resources, a concept central to the architecture of the web.
Yaks consists of a resource representation that is independent of any output type. A Yaks mapper transforms an object into a resource, which can then be serialized into whichever output format the client requested. These formats are presently supported:
- HAL
- JSON API
- Collection+JSON
- HTML
- HALO
- Transit
Table of Contents
- State of Development
- Concepts
- Mappers
- Calling Yaks
- Namespace
- Custom attribute/link/subresource handling
- Resources, Formatters, Serializers
- Formats
- Hooks
- Policy over Configuration
- Primitivizer
- Integration
- Real World Usage
- Demo
- Cookbook
- Standards Based
- How to contribute
- License
Packages
State of Development
Recent focus has been on stabilizing the core classes, improving format support, and increasing test (mutation) coverage. We are committed to a stable public API and semantic version. On the 0.x line the minor version is bumped when non-backwards compatible changes are introduced. After 1.x regular semver conventions will be used.
Concepts
Yaks is a processing pipeline, you create and configure the pipeline, then feed data through it.
yaks = Yaks.new do
default_format :hal
rel_template 'http://api.example.com/rels/{rel}'
format_options(:hal, plural_links: [:copyright])
mapper_namespace ::MyAPI
json_serializer do |data|
JSON.dump(data)
end
end
yaks.call(product)
Yaks performs this serialization in three steps
- It maps your data to a
Yaks::Resource - It formats the resource to a syntax tree representation
- It serializes to get the final output
For JSON types, the "syntax tree" is just a combination of Ruby primitives, nested arrays and hashes with strings, numbers, booleans, nils.
A Resource is an abstraction shared by all output formats. It can contain key-value attributes, RFC5988 style links, and embedded sub-resources.
To build an API you create a "mapper" for each type of object you want to represent. Yaks takes care of the rest.
For all configuration options see Yaks::Config::DSL.
See also the API Docs on rdoc.info
Mappers
Say your app has a Post object for blog posts. To serve posts over your API, define a PostMapper
class PostMapper < Yaks::Mapper
link :self, '/api/posts/{id}'
attributes :id, :title
has_one :author
has_many :comments
end
Configure a Yaks instance and start serializing!
yaks = Yaks.new
yaks.call(post)
or a bit more elaborate
yaks = Yaks.new do
default_format :json_api
rel_template 'http://api.example.com/rels/{rel}'
format_options(:hal, plural_links: [:copyright])
end
yaks.call(post, mapper: ::PostMapper, format: :hal)
Attributes
Use the attribute or attributes DSL methods to specify which attributes of your model you want to expose, as in the example above. You can override the load_attribute method to change how attributes are fetched from the model.
For example, if you are representing data that is stored in a Hash, you could do
class PostHashMapper < Yaks::Mapper
attributes :id, :body
# @param name [Symbol]
def load_attribute(name)
object[name]
end
end
The attribute method may also take a block that will be called with the context of the mapper instance. The default implementation will use the block if provided, otherwise it will first try to find a matching method for an attribute on the mapper itself, and will then fall back to calling the actual model. So you can add extra 'virtual' attributes like so :
class CommentMapper < Yaks::Mapper
attributes :body, :date
attribute :id do
"Id-#{object.id}"
end
def date
object.created_at.strftime("at %I:%M%p")
end
end
Forms
Mapper can contain form defintions, for formats that support them. The form DSL mimics the HTML5 field and attribute names.
class PostMapper < Yaks::Mapper
attributes :id, :body, :date
form :add_comment do
action '/api/comments'
method 'POST'
media_type 'application/json'
text :body
hidden :post_id, value: -> { object.id }
end
end
TODO: add more info on form element types, attributes, conditional rendering of forms, dynamic form sections, ...
Filtering
You can override #attributes, or #associations.
class SongMapper < Yaks::Mapper
attributes :title, :duration, :lyrics
has_one :artist
has_one :album
def minimal?
env['HTTP_PREFER'] =~ /minimal/
end
# @return Array<Yaks::Mapper::Attribute>
def attributes
return super.reject {|attr| attr.name.equal? :lyrics } if minimal?
super
end
# @return Array<Yaks::Mapper::Association>
def associations
return [] if minimal?
super
end
end
Links
You can specify link templates that will be expanded with model attributes. The link relation name should be a registered IANA link relation or a URL. The template syntax follows RFC6570 URI templates.
class FooMapper < Yaks::Mapper
link :self, '/api/foo/{id}'
link 'http://api.foo.com/rels/comments', '/api/foo/{id}/comments'
end
To prevent a link to be expanded, add expand: false as an option. Now the actual template will be rendered in the result, so clients can use it to generate links from.
To partially expand the template, pass an array with field names to expand. e.g.
class ProductMapper < Yaks::Mapper
link 'http://api.foo.com/rels/line_item', '/api/line_items?product_id={product_id}&quantity={quantity}', expand: [:product_id]
end
# "_links": {
# "http://api.foo.com/rels/line_item": {
# "href": "/api/line_items?product_id=273&quantity={quantity}",
# "templated": true
# }
# }
You can pass a proc instead of a template, in that case the proc will
be resolved in the context of the mapper. What this means is that, if
the proc takes no arguments, it will be evaluated with the mapper
instance as the value of self. If the proc does take an argument,
then it will receive the mapper instance, and will be evaluated as a
closure, i.e. with access to the scope in which it was defined.
class FooMapper < Yaks::Mapper
link 'http://api.foo.com/rels/go_home', -> { home_url }
# by default calls object.home_url
def home_url
object.setting('home_url')
end
end
To only include links based on certain conditions, add an :if
option, passing it a block. The block will be resolved in the context
of the mapper, as explained before.
For example, say you want to notify the consumer of your API that upon
confirming an order, the previously held cart is no longer valid, you
could use the IANA standard invalidates rel to communicate this.
class OrderMapper < Yaks::Mapper
link :invalidates, '/api/cart', if: ->{ env['api.invalidate_cart'] }
end
Associations
Use has_one for an association that returns a single object, or has_many for embedding a collection.
Options
:mapper: Use a specific for each instance, will be derived from the class name if omitted (see Policy vs Configuration):collection_mapper: For mapping the collection as a whole, this defaults to Yaks::CollectionMapper, but you can subclass it for example to add links or attributes on the collection itself:rel: Set the r
