rose

Rose (say it out loud: rows, rows, rows) is a slick Ruby DSL for reporting:

Rose.make(:worlds) do
  rows do
    column(:hello => "Hello")
    column(:world)
  end
end

class World < Struct.new(:hello, :world)
end

Rose(:worlds).bloom([World.new("Say", "what?")]).to_s

+---------------+
| Hello | world |
+---------------+
| Say   | what? |
+---------------+

Install the gem:

gem install rose

Usage

Making a Report

class Flower < Struct.new(:type, :color, :age)
end

Rose.make(:poem, :class => Flower) do
  rows do
    column(:type => "Type")
    column("Color", &:color)
  end
end

Running a Report

Rose(:poem).bloom([Flower.new(:roses, :red), Flower.new(:violets, :blue)])

+-----------------+
|  Type   | Color |
+-----------------+
| roses   | red   |
| violets | blue  |
+-----------------+

Sorting

Rose.make(:with_sort_by_age_descending, :class => Flower) {
  rows do
    column(:type => "Type")
    column(:color => "Color")
    column(:age => "Age")
  end
  sort("Age", :descending)
}

Filtering

Rose.make(:with_filter, :class => Flower) {
  rows do
    column(:type => "Type")
    column(:color => "Color")
    column(:age => "Age")
  end
  filter do |row|
    row["Color"] != "blue"
  end
}

Summarizing

Rose.make(:with_summary, :class => Flower) {
  rows do
    column(:type => "Type")
    column(:color => "Color")
  end
  summary("Type") do
    column("Color") { |colors| colors.uniq.join(", ") }
    column("Count") { |colors| colors.size }
  end
}

Pivoting

Rose.make(:with_pivot, :class => Flower) {
  rows do
    column(:type => "Type")
    column(:color => "Color")
    column(:age => "Age")
  end
  pivot("Color", "Type") do |rows|
    rows.map(&:Age).map(&:to_i).inject(0) { |sum,x| sum+x }
  end
}

Importing

Rose.make(:with_find_and_update) do
  rows do
    identity(:id => "ID")
    column(:type => "Type")
    column(:color => "Color")
    column(:age => "Age")
  end
  roots do
    # find is optional. By default will return items with item["ID"] == idy
    find do |items, idy|
      items.find { |item| item.id.to_s == idy }
    end
    update do |item, updates|
      item.color = updates["Color"]
    end
  end
end

#identity must be used for one column. Without it Rose won't be able to identify which items to update.

Manually

Rose(:with_find_and_update).photosynthesize(@flowers, {
  :updates => {
    "0" => { "Color" => "blue" }
    # ID => Updates
  }
})

CSV

Rose(:with_find_and_update).photosynthesize(@flowers, {
  :csv_file => "change_flowers.csv"
})

Preview

Rose.make(:with_preview) do
  rows do
    identity(:id => "ID")
    column(:type => "Type")
    column(:color => "Color")
    column(:age => "Age")
  end
  roots do
    preview_update do |item, updates|
      item.preview(true); item.color = updates["Color"]
    end
    update { raise Exception, "you shouldn't be calling me" }
  end
end

Rose(:with_preview).photosynthesize(@flowers, {
  :updates => {
    "0" => { "Color" => "blue" }
  },
  :preview => true
})

Rose(:with_preview).photosynthesize(@flowers, {
  :csv_file => "change_flowers.csv",
  :preview  => true
})

ActiveRecord

First, use the ActiveRecord adapter:

config.gem 'rose', :lib => 'rose/active_record'

For the most part, the ActiveRecord adapter has the same interface as the ObjectAdapter, except for the following differences:

Making a Report

Employee.rose(:department_salaries) do
  rows do
    column("Name") { |e| "#{e.firstname} #{e.lastname}" }
    column("Department") { |e| e.department.name }
    column("Salary") { |e| e.salary }
  end
  summary("Department") do
    column("Salary") { |salaries| salaries.map(&:to_i).sum }
  end
end

Running a Report

Employee.rose_for(:department_salaries, :conditions => ["salary <> ?", nil])

+----------------------+
| Department  | Salary |
+----------------------+
| Accounting  |  85000 |
| Admin       |  69000 |
| Sales       | 120000 |
| Engineering | 122000 |
| IT          |  50000 |
| Graphics    |  42000 |
+----------------------+

Employee#rose_for is a helper method that blooms on Employee.find(:all, :conditions => ["salary <> ?", nil]). If you still want direct access to your report, you can use Employee.seedlings(:department_salaries)

Importing (with Preview)

Post.rose(:for_update) {
  rows do
    identity(:guid => "ID")
    column("Title", &:title)
    column("Comments") { |item| item.comments.size }
  end

  sort("Comments", :descending)

  roots do
    find do |items, idy|
      items.find { |item| item.guid == idy }
    end
    preview_create do |idy, updates|
      post = Post.new(:guid => idy)
      post.title = updates["Title"]
      post
    end
    create do |idy, updates|
      post = create_previewer.call(idy, updates)
      post.save!
      post
    end
    preview_update do |record, updates|
      record.title = updates["Title"]
    end
    update do |record, updates|
      record.update_attribute(:title, updates["Title"])
    end
  end
}

Post.root_for(:for_update, {
  :with => {
    "1" => { "Title" => "New Title" }
  },
  :preview => true
}) # => Returns a table

Post.root_for(:for_update, {
  :with => "change_flowers.csv"
  :preview  => true
})

Other

Inspired by Machinist and factory_girl


Future

  • Documentation

Copyright

Copyright (c) 2010 Henry Hsu. See LICENSE for details.