Skip to content

Commit

Permalink
Initial implementation.
Browse files Browse the repository at this point in the history
  • Loading branch information
fnando committed Oct 17, 2011
1 parent a3e3d27 commit 8a2a249
Show file tree
Hide file tree
Showing 12 changed files with 281 additions and 3 deletions.
13 changes: 13 additions & 0 deletions Gemfile.lock
Expand Up @@ -6,7 +6,15 @@ PATH
GEM
remote: http://gems.simplesideias.com.br/
specs:
coderay (0.9.8)
diff-lcs (1.1.3)
method_source (0.6.6)
ruby_parser (~> 2.0.5)
pry (0.9.6.2)
coderay (~> 0.9.8)
method_source (~> 0.6.5)
ruby_parser (~> 2.0.5)
slop (~> 2.1.0)
rake (0.9.2)
rspec (2.7.0)
rspec-core (~> 2.7.0)
Expand All @@ -16,11 +24,16 @@ GEM
rspec-expectations (2.7.0)
diff-lcs (~> 1.1.2)
rspec-mocks (2.7.0)
ruby_parser (2.0.6)
sexp_processor (~> 3.0)
sexp_processor (3.0.7)
slop (2.1.0)

PLATFORMS
ruby

DEPENDENCIES
pry
rake
rspec (~> 2.7)
simple_presenter!
40 changes: 39 additions & 1 deletion README.rdoc
Expand Up @@ -8,7 +8,45 @@ Some description

== Usage

Usage info
class User < ActiveRecord::Base
# implements the following attributes: name, email, password_hash, password_salt
end

class UserPresenter < Presenter
expose :name, :email
end

user = UserPresenter.new(User.first)
users = UserPresenter.map(User.all)

If you're using Simple Presenter within Rails, presenters also have access to:

* route helpers: just use the <tt>routes</tt> or <tt>r</tt> methods
* view helpers: just use the <tt>helpers</tt> or <tt>h</tt> methods
* I18n methods: just use the <tt>translate</tt>, <tt>t</tt>, <tt>localize</tt> or <tt>l</tt> methods

For additional usage, check the specs.

== TO-DO

* Recognize ActiveRecord objects and automatically expose attributes used by url and form helpers (like <tt>Model.model_name</tt>, <tt>Model#to_key</tt>, and <tt>Model#to_param</tt>).
* Override <tt>respond_to?</tt> to reflect exposed attributes.

== Troubleshooting

If you're having problems because already have a class/module called Presenter that is conflicting with this gem, you can require the namespace and inherit from <tt>SimplePresenter::Base</tt>.

require "simple_presenter/namespace"

class UserPresenter < SimplePresenter::Base
end

If you're using Rails/Bundler or something like that, remember to override the <tt>:require</tt> option.

# Gemfile
source :rubygems

gem "simple_presenter", :require => "simple_presenter/namespace"

== Maintainer

Expand Down
92 changes: 92 additions & 0 deletions lib/simple_presenter/base.rb
@@ -1,4 +1,96 @@
module SimplePresenter
class Base
# Define how many subjects this presenter will receive.
# Each subject will create a private method with the same name.
#
# The first subject name will be used as default, thus isn't required as <tt>:with</tt>
# option on the SimplePresenter::Base.expose method.
#
# class CommentPresenter < Presenter
# subjects :comment, :post
# expose :body # will expose comment.body
# expose :title, :with => :post # will expose post.title
# end
#
def self.subjects(*names)
@subjects ||= [:subject]
@subjects = names unless names.empty?
@subjects
end

# This method will return a presenter for each item of collection.
#
# users = UserPresenter.map(User.all)
#
# If your presenter accepts more than one subject, you can provided
# them as following parameters.
#
# comments = CommentPresenter.map(post.comment.all, post)
#
def self.map(collection, *subjects)
collection.map {|item| new(item, *subjects)}
end

# The list of attributes that will be exposed.
#
# class UserPresenter < Presenter
# expose :name, :email
# end
#
# You can also expose an attribute from a composition.
#
# class CommentPresenter < Presenter
# expose :body, :created_at
# expose :name, :with => :user
# end
#
# The presenter above will expose the methods +body+, +created_at+, and +user_name+.
#
def self.expose(*attrs)
options = attrs.pop if attrs.last.kind_of?(Hash)
options ||= {}

attrs.each do |attr_name|
subject = options.fetch(:with, nil)
method_name = [subject, attr_name].compact.join("_")

class_eval <<-RUBY, __FILE__, __LINE__ + 1
def #{method_name} # def user_name
proxy_message(#{subject.inspect}, "#{attr_name}") # proxy_message("user", "name")
end # end
RUBY
end
end

# It assigns the subjects.
#
# user = UserPresenter.new(User.first)
#
# You can assign several subjects if you want.
#
# class CommentPresenter < Presenter
# subject :comment, :post
# expose :body
# expose :title, :with => :post
# end
#
# comment = CommentPresenter.new(Comment.first, Post.first)
#
# If the :with option specified to one of the subjects, then the default subject is bypassed.
# Otherwise, it will be proxied to the default subject.
#
def initialize(*subjects)
self.class.subjects.each_with_index do |name, index|
instance_variable_set("@#{name}", subjects[index])
end
end

private
def proxy_message(subject_name, method)
subject_name ||= self.class.subjects.first
subject = instance_variable_get("@#{subject_name}")
subject = instance_variable_get("@#{self.class.subjects.first}").__send__(subject_name) unless subject || self.class.subjects.include?(subject_name)
subject.respond_to?(method) ? subject.__send__(method) : nil
end
end
end
2 changes: 2 additions & 0 deletions lib/simple_presenter/namespace.rb
@@ -1,4 +1,6 @@
module SimplePresenter
autoload :Base, "simple_presenter/base"
autoload :Version, "simple_presenter/version"

require "simple_presenter/rails" if defined?(Rails)
end
28 changes: 28 additions & 0 deletions lib/simple_presenter/rails.rb
@@ -0,0 +1,28 @@
module SimplePresenter
class Base
private
def translate(*args, &block)
I18n.t(*args, &block)
end

alias_method :t, :translate

def localize(*args, &block)
I18n.l(*args, &block)
end

alias_method :l, :localize

def routes
Rails.application.routes.url_helpers
end

alias_method :r, :routes

def helpers
ApplicationController.helpers
end

alias_method :h, :helpers
end
end
4 changes: 2 additions & 2 deletions simple_presenter.gemspec
Expand Up @@ -9,15 +9,15 @@ Gem::Specification.new do |s|
s.authors = ["Nando Vieira"]
s.email = ["fnando.vieira@gmail.com"]
s.homepage = "http://rubygems.org/gems/simple_presenter"
s.summary = "TODO: Write a description"
s.summary = "A simple presenter/facade/decorator/whatever implementation."
s.description = s.summary

s.files = `git ls-files`.split("\n")
s.test_files = `git ls-files -- {test,spec,features}/*`.split("\n")
s.executables = `git ls-files -- bin/*`.split("\n").map{ |f| File.basename(f) }
s.require_paths = ["lib"]

# s.add_dependency "activesupport"
s.add_development_dependency "rake"
s.add_development_dependency "rspec", "~> 2.7"
s.add_development_dependency "pry"
end
78 changes: 78 additions & 0 deletions spec/base_spec.rb
@@ -0,0 +1,78 @@
require "spec_helper"

describe SimplePresenter::Base do
describe ".expose" do
context "not using :with option" do
subject { UserPresenter.new }

it { should respond_to(:name) }
it { should respond_to(:email) }
it { should_not respond_to(:password_hash) }
it { should_not respond_to(:password_salt) }
end

context "using :with option" do
subject { CommentPresenter.new }

it { should respond_to(:user_name) }
end
end

describe ".subjects" do
context "using defaults" do
let(:user) { stub :name => "John Doe", :email => "john@doe.com" }
subject { UserPresenter.new(user) }

its(:name) { should == "John Doe" }
its(:email) { should == "john@doe.com" }
end

context "specifying several subjects" do
let(:user) { stub :name => "John Doe" }
let(:comment) { stub :body => "Some comment", :user => user }
let(:post) { stub :title => "Some post" }
subject { CommentPresenter.new(comment, post) }

its(:body) { should == "Some comment" }
its(:post_title) { should == "Some post" }
its(:user_name) { should == "John Doe" }
end

context "when subjects are nil" do
let(:comment) { stub :body => "Some comment" }
subject { CommentPresenter.new(comment, nil) }

its(:post_title) { should be_nil }
end
end

describe ".map" do
context "wraps a single subject" do
let(:user) { stub :name => "John Doe" }
subject { UserPresenter.map([user])[0] }

it { should be_a(UserPresenter) }
its(:name) { should == "John Doe" }
end

context "wraps several subjects" do
let(:comment) { stub :body => "Some comment" }
let(:post) { stub :title => "Some post" }
let(:user) { stub :name => "John Doe" }
subject { CommentPresenter.map([comment], post)[0] }

it { should be_a(CommentPresenter) }
its(:body) { should == "Some comment" }
its(:post_title) { should == "Some post" }
end
end

describe "#initialize" do
let(:user) { mock }
subject { UserPresenter.new(user) }

it "assigns the subject" do
subject.instance_variable_get("@subject").should == user
end
end
end
3 changes: 3 additions & 0 deletions spec/spec_helper.rb
Expand Up @@ -4,3 +4,6 @@

require "simple_presenter"

Dir[File.dirname(__FILE__) + "/support/**/*.rb"].each do |file|
require file
end
7 changes: 7 additions & 0 deletions spec/support/comment.rb
@@ -0,0 +1,7 @@
class Comment
attr_accessor :body, :created_at, :user

def initialize(attrs = {})
attrs.each {|name, value| __send__("#{name}=", value)}
end
end
7 changes: 7 additions & 0 deletions spec/support/comment_presenter.rb
@@ -0,0 +1,7 @@
class CommentPresenter < Presenter
expose :body
expose :name, :with => :user
expose :title, :with => :post

subjects :comment, :post
end
7 changes: 7 additions & 0 deletions spec/support/user.rb
@@ -0,0 +1,7 @@
class User
attr_accessor :name, :email, :password_salt, :password_hash

def initialize(attrs = {})
attrs.each {|name, value| __send__("#{name}=", value)}
end
end
3 changes: 3 additions & 0 deletions spec/support/user_presenter.rb
@@ -0,0 +1,3 @@
class UserPresenter < Presenter
expose :name, :email
end

0 comments on commit 8a2a249

Please sign in to comment.