Class: Presentability::Presenter
- Inherits:
-
Object
- Object
- Presentability::Presenter
- Extended by:
- Loggability
- Defined in:
- lib/presentability/presenter.rb
Overview
A presenter (facade) base class.
### Declaring Presenters
When you declare a presenter in a Presentability collection, the result is a subclass of Presentability::Presenter. The main way of defining a Presenter’s functionality is via the ::expose method, which marks an attribute of the underlying entity object (the “subject”) for exposure.
class MyPresenter < Presentability::Presenter
expose :name
end
# Assuming `entity_object' has a "name" attribute...
presenter = MyPresenter.new( entity_object )
presenter.apply
# => { :name => "entity name" }
### Presenter Collections
Setting up classes manually like this is one option, but Presentability also lets you set them up as a collection, which is what further examples will assume for brevity:
module MyPresenters
extend Presentability
presenter_for( EntityObject ) do
expose :name
end
end
### Complex Exposures
Sometimes you want to do more than just use the presented entity’s values as-is. There are a number of ways to do this.
The first of these is to provide a block when exposing an attribute. The subject of the presenter is available to the block via the ‘subject` method:
require 'time'
presenter_for( LogEvent ) do
# Turn Time objects into RFC2822-formatted time strings
expose :timestamp do
self.subject..rfc2822
end
end
You can also declare the exposure using a regular method with the same name:
require 'time'
presenter_for( LogEvent ) do
# Turn Time objects into RFC2822-formatted time strings
expose :timestamp
def
return self.subject..rfc2822
end
end
This can be used to add presence checks:
require 'time'
presenter_for( LogEvent ) do
# Require that presented entities have an `id` attribute
expose :id do
id = self.subject.id or raise "no `id' for %p" % [ self.subject ]
raise "`id' for %p is blank!" % [ self.subject ] if id.blank?
return id
end
end
or conditional exposures:
presenter_for( Acme::Product ) do
# Truncate the long description if presented as part of a collection
expose :detailed_description do
desc = self.subject.detailed_description
if self.[:in_collection]
return desc[0..15] + '...'
else
return desc
end
end
end
### Exposure Aliases
If you want to expose a field but use a different name in the resulting data structure, you can use the ‘:as` option in the exposure declaration:
presenter_for( LogEvent ) do
expose :timestamp, as: :created_at
end
presenter = MyPresenter.new( log_event )
presenter.apply
# => { :created_at => '2023-02-01 12:34:02.155365 -0800' }
Constant Summary collapse
- DEFAULT_EXPOSURE_OPTIONS =
The exposure options used by every exposure unless overridden
{}.freeze
Instance Attribute Summary collapse
-
#exposures ⇒ Object
:singleton-method: exposures The Hash of exposures declared by this class.
-
#options ⇒ Object
readonly
The presentation options.
-
#subject ⇒ Object
readonly
The subject of the presenter, the object that is delegated to when building the representation.
Class Method Summary collapse
-
.expose(name, **options, &block) ⇒ Object
Set up an exposure that will delegate to the attribute of the subject with the given
name
. -
.expose_collection(name, **options, &block) ⇒ Object
Set up an exposure of a collection with the given
name
. -
.generate_expose_method(name, **options) ⇒ Object
Generate the body an exposure method that delegates to a method with the same
name
on its subject. -
.inherited(subclass) ⇒ Object
Enable instantiation by subclasses.
-
.method_added(method_name) ⇒ Object
Method definition hook – hook up new methods with the same name as an exposure to its :call option.
Instance Method Summary collapse
-
#apply(presenters) ⇒ Object
Apply the exposures to the subject and return the result.
-
#initialize(subject, options = {}) ⇒ Presenter
constructor
Create a new Presenter for the given
subject
. -
#inspect ⇒ Object
Return a human-readable representation of the object suitable for debugging.
-
#skip_exposure?(name) ⇒ Boolean
Returns
true
if the exposure with the specifiedname
should be skipped for the current #subject and #options.
Constructor Details
#initialize(subject, options = {}) ⇒ Presenter
Create a new Presenter for the given subject
.
209 210 211 212 |
# File 'lib/presentability/presenter.rb', line 209 def initialize( subject, ={} ) @subject = subject @options = end |
Instance Attribute Details
#exposures ⇒ Object
:singleton-method: exposures The Hash of exposures declared by this class
147 148 149 |
# File 'lib/presentability/presenter.rb', line 147 def exposures @exposures end |
#options ⇒ Object (readonly)
The presentation options
226 227 228 |
# File 'lib/presentability/presenter.rb', line 226 def @options end |
#subject ⇒ Object (readonly)
The subject of the presenter, the object that is delegated to when building the representation.
222 223 224 |
# File 'lib/presentability/presenter.rb', line 222 def subject @subject end |
Class Method Details
.expose(name, **options, &block) ⇒ Object
Set up an exposure that will delegate to the attribute of the subject with the given name
.
152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 |
# File 'lib/presentability/presenter.rb', line 152 def self::expose( name, **, &block ) name = name.to_sym = DEFAULT_EXPOSURE_OPTIONS.merge( ) self.define_method( name, &block ) if block unless self.instance_methods( true ).include?( name ) method_body = self.generate_expose_method( name, ** ) define_method( name, &method_body ) end if (exposure_alias = [:as]) && self.exposures.key?( exposure_alias ) raise ScriptError, "alias %p collides with another exposure" % [ exposure_alias ] end self.log.debug "Setting up exposure %p, options = %p" % [ name, ] self.exposures[ name ] = end |
.expose_collection(name, **options, &block) ⇒ Object
Set up an exposure of a collection with the given name
. This means it will have the :in_collection option set by default.
174 175 176 177 |
# File 'lib/presentability/presenter.rb', line 174 def self::expose_collection( name, **, &block ) = .merge( unless: :in_collection ) self.expose( name, **, &block ) end |
.generate_expose_method(name, **options) ⇒ Object
Generate the body an exposure method that delegates to a method with the same name
on its subject.
182 183 184 185 186 187 |
# File 'lib/presentability/presenter.rb', line 182 def self::generate_expose_method( name, ** ) self.log.debug "Generating a default delegation exposure method for %p" % [ name ] return lambda do return self.subject.send( __method__ ) end end |
.inherited(subclass) ⇒ Object
Enable instantiation by subclasses.
137 138 139 140 141 |
# File 'lib/presentability/presenter.rb', line 137 def self::inherited( subclass ) super subclass.public_class_method( :new ) subclass.exposures = {} end |
.method_added(method_name) ⇒ Object
Method definition hook – hook up new methods with the same name as an exposure to its :call option.
192 193 194 195 196 197 198 199 200 201 |
# File 'lib/presentability/presenter.rb', line 192 def self::method_added( method_name ) super return unless self.exposures if self.exposures.key?( method_name ) self.log.debug "Exposing %p via a new presenter method." % [ method_name ] self.exposures[ method_name ][ :call ] = self.instance_method( method_name ) end end |
Instance Method Details
#apply(presenters) ⇒ Object
Apply the exposures to the subject and return the result.
230 231 232 233 234 235 236 237 238 239 240 241 242 243 |
# File 'lib/presentability/presenter.rb', line 230 def apply( presenters ) result = self.empty_representation self.class.exposures.each do |name, | next if self.skip_exposure?( name ) self.log.debug "Presenting %p" % [ name ] value = self.method( name ).call value = presenters.present( value, ** ) key = .key?( :as ) ? [:as] : name result[ key.to_sym ] = value end return result end |
#inspect ⇒ Object
Return a human-readable representation of the object suitable for debugging.
257 258 259 |
# File 'lib/presentability/presenter.rb', line 257 def inspect return "#<Presentability::Presenter:%#0x for %p>" % [ self.object_id / 2, self.subject ] end |
#skip_exposure?(name) ⇒ Boolean
Returns true
if the exposure with the specified name
should be skipped for the current #subject and #options.
248 249 250 251 252 253 |
# File 'lib/presentability/presenter.rb', line 248 def skip_exposure?( name ) = self.class.exposures[ name ] or return true return ([:if] && !self.[ [:if] ]) || ([:unless] && self.[ [:unless] ]) end |