Resource-Oriented Architectures in Ruby.
Roar is a framework for parsing and rendering REST documents. Nothing more.
Representers let you define your API document structure and semantics. They allow both rendering representations from your models and parsing documents to update your Ruby objects. The bi-directional nature of representers make them interesting for both server and client usage.
Roar comes with built-in JSON, JSON-HAL, JSON-API and XML support. Its highly modular architecture provides features like coercion, hypermedia, HTTP transport, client caching and more.
Roar is completely framework-agnostic and loves being used in web kits like Rails, Webmachine, Sinatra, Padrino, etc. If you use Rails, consider roar-rails for an enjoyable integration.
Roar is part of the Trailblazer project. Please buy the book to support the development. Several chapters will be dedicated to Roar, its integration into operations, hypermedia formats and client-side usage.
Roar is just a thin layer on top of the representable gem. While Roar gives you a DSL and behaviour for creating hypermedia APIs, representable implements all the mapping functionality.
If in need for a feature, make sure to check the representable API docs first.
The roar gem runs with all Ruby versions >= 1.9.3.
gem 'roar'Roar does not bundle dependencies for JSON and XML.
If you want to use JSON, add the following to your Gemfile:
gem 'multi_json'If you want to use XML, add the following to your Gemfile:
gem 'nokogiri'Let's see how representers work. They're fun to use.
require 'roar/json'
module SongRepresenter
include Roar::JSON
property :title
endAPI documents are defined using a representer module or decorator class. You can define plain attributes using the ::property method.
Now let's assume we'd have Song which is an ActiveRecord class. Please note that Roar is not limited to ActiveRecord. In fact, it doesn't really care whether it's representing ActiveRecord, Datamapper or just an OpenStruct instance.
class Song < ActiveRecord
endTo render a document, you apply the representer to your model.
song = Song.new(title: "Fate")
song.extend(SongRepresenter)
song.to_json #=> {"title":"Fate"}Here, the representer is injected into the actual model and gives us a new #to_json method.
The cool thing about representers is: they can be used for rendering and parsing. See how easy updating your model from a document is.
song = Song.new
song.extend(SongRepresenter)
song.from_json('{"title":"Linoleum"}')
song.title #=> LinoleumAgain, #from_json comes from the representer and just updates the known properties.
Unknown attributes in the parsed document are simply ignored, making half-baked solutions like strong_parameters redundant.
Many people dislike #extend due to eventual performance issue or object pollution. If you're one of those, just go with a decorator representer. They almost work identical to the module approach we just discovered.
require 'roar/decorator'
class SongRepresenter < Roar::Decorator
include Roar::JSON
property :title
endIn place of a module you use a class, the DSL inside is the same you already know.
song = Song.new(title: "Medicine Balls")
SongRepresenter.new(song).to_json #=> {"title":"Medicine Balls"}Here, the song objects gets wrapped (or "decorated") by the decorator. It is treated as immutuable - Roar won't mix in any behaviour.
Note that decorators and representer modules have identical features. You can parse, render, nest, go nuts with both of them.
However, in this README we'll use modules to illustrate this framework.
Roar (or rather representable) also allows to map collections in documents.
module SongRepresenter
include Roar::JSON
property :title
collection :composers
endWhere ::property knows how to handle plain attributes, ::collection does lists.
song = Song.new(title: "Roxanne", composers: ["Sting", "Stu Copeland"])
song.extend(SongRepresenter)
song.to_json #=> {"title":"Roxanne","composers":["Sting","Stu Copeland"]}And, yes, this also works for parsing: from_json will create and populate the array of the composers attribute.
Now what if we need to tackle with collections of Songs? We need to implement an Album class.
class Album < ActiveRecord
has_many :songs
endAnother representer to represent.
module AlbumRepresenter
include Roar::JSON
property :title
collection :songs, extend: SongRepresenter, class: Song
endBoth ::property and ::collection accept options for nesting representers into representers.
The extend: option tells Roar which representer to use for the nested objects (here, the array items of the album.songs field). When parsing a document class: defines the nested object type.
Consider the following object setup.
album = Album.new(title: "True North")
album.songs << Song.new(title: "The Island")
album.songs << Song.new(:title => "Changing Tide")You apply the AlbumRepresenter and you get a nested document.
album.extend(AlbumRepresenter)
album.to_json #=> {"title":"True North","songs":[{"title":"The Island"},{"title":"Changing Tide"}]}This works vice-versa.
album = Album.new
album.extend(AlbumRepresenter)
album.from_json('{"title":"Indestructible","songs":[{"title":"Tropical London"},{"title":"Roadblock"}]}')
puts album.songs[1] #=> #<Song title="Roadblock">The nesting of two representers can map composed object as you find them in many many APIs.
In case you're after virtual nesting, where a nested block in your document still maps to the same outer object, check out the ::nested method.
Sometimes you don't wanna create two separate representers - although it makes them reusable across your app. Use inline representers if you're not intending this.
module AlbumRepresenter
include Roar::JSON
property :title
collection :songs, class: Song do
property :title
end
endThis will give you the same rendering and parsing behaviour as in the previous example with just one module.
Usually, when parsing, nested objects are created from scratch. If you want nested objects to be updated instead of being newly created, use parse_strategy:.
module AlbumRepresenter
include Roar::JSON
property :title
collection :songs, extend: SongRepresenter, parse_strategy: :sync
endThis will advise Roar to update existing songs.
album.songs[0].object_id #=> 81431220
album.from_json('{"title":"True North","songs":[{"title":"Secret Society"},{"title":"Changing Tide"}]}')
album.songs[0].title #=> Secret Society
album.songs[0].object_id #=> 81431220Roar didn't create a new Song instance but updated its attributes, only.
We're currently working on better strategies to easily implement POST and PUT semantics in your APIs without having to worry about the nitty-gritties.
Roar provides coercion with the virtus gem.
require 'roar/coercion'
module SongRepresenter
include Roar::JSON
include Roar::Coercion
property :title
property :released_at, type: DateTime
endThe :type option allows to set a virtus-compatible type.
song = Song.new
song.extend(SongRepresenter)
song.from_json('{"released_at":"1981/03/31"}')
song.released_at #=> 1981-03-31T00:00:00+00:00Roar/representable gives you many more mapping features like renaming attributes, wrapping, passing options, etc.
Roar comes with built-in support for embedding and processing hypermedia in your documents.
module SongRepresenter
include Roar::JSON
include Roar::Hypermedia
property :title
link :self do
"http://songs/#{title}"
end
endThe Hypermedia feature allows declaring links using the ::link method. In the block, you have access to the represented model. When using representer modules, the block is executed in the model's context.
However, when using decorators, the context is the decorator instance, allowing you to access additional data. Use represented to retrieve model data.
class SongRepresenter < Roar::Decorator
# ..
link :self do
"http://songs/#{represented.title}"
end
endThis will render links into your representation.
song.extend(SongRepresenter)
song.to_json #=> {"title":"Roxanne","links":[{"rel":"self","href":"http://songs/Roxanne"}]}Per default, links are pushed into the hash using the links key. Link blocks are executed in represented context, allowing you to call any instance method of your model (here, we call #title).
Also, note that roar-rails allows using URL helpers in link blocks.
Sometimes you need more data in the link block. Data that's not available from the represented model.
module SongRepresenter
include Roar::JSON
property :title
link :self do |opts|
"http://#{opts[:base_url]}songs/#{title}"
end
endPass this data to the rendering method.
song.to_json(base_url: "localhost:3001/")Any options passed to #to_json will be available as block arguments in the link blocks.
Since we defined hypermedia attributes in the representer we can also consume this hypermedia when we parse documents.
song.from_json('{"title":"Roxanne","links":[{"rel":"self","href":"http://songs/Roxanne"}]}')
song.links[:self].href #=> "http://songs/Roxanne"Reading link attributes works by using #links[] on the consuming instance.
This allows an easy way to discover hypermedia and build navigational logic on top.
While Roar comes with a built-in hypermedia format, there's official media types that are widely recognized. Roar currently supports HAL and Collection+JSON. Support for Siren and JSON-API is planned when there's sponsors.
Simply by including a module you make your representer understand the media type. This makes it easy to change formats during evaluation.
The HAL format is a simple media type that defines embedded resources and hypermedia.
require 'roar/json/hal'
module SongRepresenter
include Roar::JSON::HAL
property :title
link :self do
"http://songs/#{title}"
end
endDocumentation for HAL can be found in the API docs.
Make sure you understand the different contexts for links when using decorators.
Including the Roar::JSON::HAL module adds some more DSL methods to your module. It still allows using ::link but treats them slightly different.
song.to_json
#=> {"title":"Roxanne","_links":{"self":{"href":"http://songs/Roxanne"}}}According to the HAL specification, links are now key with their rel attribute under the _links key.
Parsing works like-wise: Roar will use the same setters as before but it knows how to read HAL.
Nested, or embedded, resources can be defined using the :embedded option.
module AlbumRepresenter
include Roar::JSON::HAL
property :title
collection :songs, class: Song, embedded: true do
property :title
end
endTo embed a resource, you can use an inline representer or use :extend to specify the representer name.
album.to_json
#=> {"title":"True North","_embedded":{"songs":[{"title":"The Island"},{"title":"Changing Tide"}]}}HAL keys nested resources under the _embedded key and then by their type.
All HAL features in Roar are discussed in the API docs, including array links.
Roar also supports JSON-API - yay! It can render and parse singular and collection documents.
Note that you need representable >= 2.1.4 in your Gemfile.
A minimal representation can be defined as follows.
require 'roar/json/json_api'
module SongsRepresenter
include Roar::JSON::JSONAPI
type :songs
property :id
property :title
endProperties of the represented model are defined in the root level.
You can add links to linked models within the resource section.
module SongsRepresenter
# ...
has_one :composer
has_many :listeners
endGlobal links can be added using the familiar ::link method (this is still WIP as the DSL is not final).
module SongsRepresenter
# ...
link "songs.album" do
{
type: "album",
href: "http://example.com/albums/{songs.album}"
}
end
endTo add compound models into the document, use ::compound.
module SongsRepresenter
# ...
compound do
property :album do
property :id
property :title
end
collection :musicians do
property :name
end
endMeta data can be included into the rendered collection document in two ways. Please note that parsing the meta field is not implemented, yet, as I wasn't sure if people need it.
You can define meta data on your collection object and then let Roar compile it.
module SongsRepresenter
# ..
meta do
property :page
property :total
endYour collection object has to expose those methods.
collection.page #=> 1
collection.total #=> 12This will render the {"meta": {"page": 1, "total": 12}} hash into the JSON-API document.
Another way is to provide the complete meta data hash when rendering. You must not define any meta properties in the representer when using this approach.
collection.to_json("meta" => {page: params["page"], total: collection.size})If you need more functionality (and parsing), please let us know.
As JSON-API per definition can represent singular models and collections you have two entry points.
SongsRepresenter.prepare(Song.find(1)).to_json
SongsRepresenter.prepare(Song.new).from_json("..")Singular models can use the representer module directly.
SongsRepresenter.for_collection.prepare([Song.find(1), Song.find(2)]).to_json
SongsRepresenter.for_collection.prepare([Song.new, Song.new]).from_json("..")Parsing currently works great with singular documents - for collections, we are still working out how to encode the application semantics. Feel free to help.
The Collection+JSON media format defines document format and semantics for requests. It is currently experimental as we're still exploring how we optimize the support with Roar. Let us know if you're using it.
module SongRepresenter
include Roar::JSON::CollectionJSON
version "1.0"
href { "http://localhost/songs/" }
property :title
items(:class => Song) do
href { "//songs/#{title}" }
property :title, :prompt => "Song title"
link(:download) { "//songs/#{title}.mp3" }
end
template do
property :title, :prompt => "Song title"
end
queries do
link :search do
{:href => "//search", :data => [{:name => "q", :value => ""}]}
end
end
endIt renders a document following the Collection+JSON specs.
#=> {"collection":{
"template":{"data":[{"name":"title","value":null}]},
"queries":[{"rel":"search","href":"//search","data":[{"name":"q","value":""}]}],
"version":"1.0",
"href":"http://localhost/songs/",
"title":"Roxanne",
"items":null}}
We have big plans with this media format, as the object model in Roar plays nicely with Collection+JSON's API semantics.
Being a bi-directional mapper that does rendering and parsing, Roar representers are perfectly suitable for use in clients, too. In many projects, representers are shared as gems between server and client.
Consider the following shared representer.
module SongRepresenter
include Roar::JSON
include Roar::Hypermedia
property :title
property :id
link :self do
"http://songs/#{title}"
end
endIn a client where you don't have access to the database it is common to use OpenStruct classes as domain objects.
require 'roar/client'
class Song < OpenStruct
include Roar::JSON
include SongRepresenter
include Roar::Client
endThe Client module mixes all necessary methods into the client class, e.g. it provides HTTP support
song = Song.new(title: "Roxanne")
song.post(uri: "http://localhost:4567/songs", as: "application/json")
song.id #=> 42What happens here?
- You're responsible for initializing the client object with attributes. This can happen with in the constructor or using accessors.
postwill use the includedSongRepresenterto compile the document using#to_json.- The document gets
POSTed to the passed URL. - If a document is returned, it is deserialized and the client's attributes are updated.
This is a very simple but efficient mechanism for working with representations in a client application.
Roar works with all HTTP request types, check out GET.
song = Client::Song.new
song.get(uri: "http://localhost:4567/songs/1", as: "application/json")
song.title #=> "Roxanne"
song.links[:self].href #=> http://localhost/songs/1As GET is not supposed to send any data, you can use #get on an empty object to populate it with the server data.
Roar supports SSL connections - they are automatically detected via the protocol.
song.get(uri: "https://localhost:4567/songs/1")The HTTP verbs allow you to specify credentials for HTTP basic auth.
song.get(uri: "http://localhost:4567/songs/1", basic_auth: ["username", "secret_password"])(Only currently supported with Net:Http)
song.get(uri: "http://localhost:4567/songs/1", pem_file: "/path/to/client/cert.pem", ssl_verify_mode: OpenSSL::SSL::VERIFY_PEER)Note: ssl_verify_mode is not required and will default to OpenSSL::SSL::VERIFY_PEER)
All verbs yield the request object before the request is sent, allowing to modify it. It is a Net::HTTP::Request instance (unless you use Faraday).
song.get(uri: "http://localhost:4567/songs/1") do |req|
req.add_field("Cookie", "Yumyum")
endIn case of a non-2xx response status, #get and friends raise a Roar::Transport::Error exception. The original response can be accessed as follows.
song.get(uri: "http://localhost/songs1") # not-existing.
rescue Roar::Transport::Error => exception
exception.response.code #=> 404Roar also comes with XML support.
module SongRepresenter
include Roar::XML
include Roar::Hypermedia
property :title
property :id
link :self do
"http://songs/#{title}"
end
endInclude the Roar::XML engine and get bi-directional XML for your objects.
song = Song.new(title: "Roxanne", id: 42)
song.extend(XML::SongRepresenter)
song.to_xmlNote that you now use #to_xml and #from_xml.
<song>
<title>Roxanne</title>
<id>42</id>
<link rel="self" href="http://songs/Roxanne"/>
</song>Please consult the representable XML documentation for all its great features.
Questions? Need help? Free 1st Level Support on irc.freenode.org#roar ! We also have a mailing list, yiha!
Roar is released under the MIT License.