Project

compositor

0.01
Repository is archived
No commit activity in last 3 years
No release in over 3 years
Define simple "compositor" classes that represent your domain objects in terms of Hashes and Arrays, and then allows you to construct complex JSON API responses using compact DSL.
2005
2006
2007
2008
2009
2010
2011
2012
2013
2014
2015
2016
2017
2018
2019
2020
2021
2022
2023
2024
 Dependencies

Development

Runtime

>= 0
 Project Readme

Compositor

Gem Version Build status Code Climate

A Composite Design Pattern with a neat DSL for constructing trees of objects in order to render them as a Hash, and subsequently JSON. Used by Wanelo to generate all JSON API responses by compositing multiple objects together in API responses, converting to a plain ruby Hash (or an Array) and then using OJ gem to convert Hash to JSON.

The performance of this approach to generate JSON was faster than using RABL in our limited testing, although if performance is not an issue for you, RABL is still an excellent choice, and served us well for almost a year.

Installation

Add this line to your application's Gemfile:

gem 'compositor'

And then execute:

$ bundle

Or install it yourself as:

$ gem install compositor

Usage

For each model that needs a hash/json representation you need to create a ruby class that subclasses Composite::Leaf, adds some custom state that's important for rendering that object in addition to context, implements a proper constructor (see example), and finally implement the main rendering #to_hash method (used as the "operation" in the Composite pattern terminology).

The context variable is a reference to an object holding necessary helpers for generating JSON, for example Rails Controllers expose a view_context instance, which contains helper methods necessary to generate application URLs.

Outside of Rails application, context can be any other object holding application helpers or state. All subclasses of Compositor::Leaf such as UserCompositor inherit context attribute and accessors, and so can use the context in generating URLs, or calling any other application helpers.

We recommend that you place your Compositor classes in eg app/compositors/* directory, which defines one (or more than one) compositor(s) per model class you will be rendering.

# File: app/compositors/user_compositor.rb
class UserCompositor < Compositor::Leaf
  attr_accessor :user

  def initialize(context, user, attrs = {})
    super(context, attrs)
    self.user = user
  end

  def to_hash
    {
        id: user.id,
        username: user.username,
        location: user.location,
        bio: user.bio,
        url: user.url,
        image_url: context.image_path(user.avatar),  # using context to generate URL path from routes
        ...
    }
  end
end

You could create this class directly, as in

   uc = UserCompositor.new(view_context, user, {})
   uc.to_hash # => returns a Hash representation
   uc.to_json # => calls to_hash, and then renders JSON

But constructing trees of objects that represent a complex API responses requires a lot more than that, such as constructing lists (arrays) or maps (hashes) of objects, and deciding which order they appear, and whether each inner Hash comes with a "root" element, such as :product => { :id => 1, ... } where :product is the root element.

So here is how to create a list of users in this way, but explicitly declaring classes:

   compositor = ListCompositor.new(view_context,
                                   :collection => @users.map { |user|
                                     UserCompositor.new(view_context, user, { :root => true })
                                   },
                                   :root => :users)

When calling to_hash on the top level compositor, we get:

:users => {
  :user => {
      id: 1234,
      username: "kigster",
      location: "San Francisco",
      bio: "",
      url: "",
      image_url: "http://cdn-app.domain.com/kigster/avatar/200.jpg"
  },
  :user => {
      id: 1235,
      username: "johnny",
      location: "Sunnyvale",
      bio: "",
      url: "",
      image_url: "http://cdn-app.domain.com/johnny/avatar/200.jpg"
  }
}

So this is how you can assemple multiple compositors together without the DSL.

But the real power of this gem is in the additional DSL class, that dramatically simplifies definition of complex responses, as described below.

Using the DSL

UserCompositor class, when defined, automatically adds a user method to the DSL class, which effictively instantiates the new UserCompositor instance, passing the context into it automatically.

Using built-in MapCompositor and ListCompositor we can construct multiple objects into a larger hierarchy.

In the example below, an application is assumed to define StoreCompositorand ProductCompositor classes similar to UserCompositor, but wrapping Store and Product ActiveRecord models.

   compositor = Compositor::DSL.create(context) do
     map do
       store @store, root: :store
       user @user, root: :user
       list collection: @products, root: :products do |p|
         product p
       end
     end
   end

   # now we can call to_hash or to_json on the compositor:
   puts compositor.to_hash # =>
   {
      :store => {
         id: 12354,
         name: "amazon.com",
         url: "http://www.amazon.com",

         ..
      },
      :user => {
         id: 1234,
         username: "kigster",
         location: "San Francisco",
         bio: "",
         url: "",
         image_url: "http://cdn-app.domain.com/kigster/avatar/200.jpg"
      },
      :products => [
           { id: 1234, :name => "Awesome Product", ... },
           { id: 4325, :name => "Another Awesome Product", ... }
      }
   }

Inside the list definition above, @products is a collection of Products, ActiveRecord objects, and the block maps each to a Compositor using product method, registered by ProductCompositor.

Instance Variables

One thing to note, is that when Compositor::DSL is used, the gem copies all instance variables from the context into the DSL instance, so in the above example instance variable @user was defined on view_context (by Rails, which copies them from the Controller instance), and so became automatically available inside DSL. Note that all instance variables must be defined before the DSL instance is created.

Method Names in the DSL

Compositor will extract the full name of the class and place that name into the DSL. For example, MyModule::UserCompositor will define a my_module_user method in the DSL. To create a method called user, the standard convention is to define a class in the global namespace called UserCompositor.

If you prefer to have your own Compositor class hierarchy, or just compositors that should not be added to the DSL, you can name the classes starting with Abstract, such as MyModule::AbstractCompositor.

Performance

Note of caution: despite the fact that typical DSL generation can take mere 50-100 microseconds, defining complex responses with DSL does carry a performance penantly of about 50% (we measured it!). Which generally means that generating multiple Composite objects in a loop using the DSL is probably not recommended, but doing it once per web/API request is completely reasonable.

Contributing

  1. Fork it
  2. Create your feature branch (git checkout -b my-new-feature)
  3. Commit your changes (git commit -am 'Added some feature')
  4. Push to the branch (git push origin my-new-feature)
  5. Create new Pull Request

Maintainers

Konstantin Gredeskoul (@kigster) and Paul Henry (@letuboy)

(c) 2013, All rights reserved, distributed under MIT license.