presentability 0.4.0 → 0.6.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- checksums.yaml.gz.sig +0 -0
- data/GettingStarted.md +234 -0
- data/History.md +18 -0
- data/README.md +3 -1
- data/lib/presentability/presenter.rb +124 -2
- data/lib/presentability.rb +175 -7
- data/lib/roda/plugins/presenters.rb +57 -0
- data/spec/presentability/presenter_spec.rb +24 -5
- data/spec/presentability_spec.rb +196 -9
- data/spec/roda/plugins/presenters_spec.rb +71 -0
- data/spec/spec_helper.rb +9 -0
- data.tar.gz.sig +0 -0
- metadata +32 -17
- metadata.gz.sig +0 -0
- data/Presentability.md +0 -105
- data/Presenter.md +0 -109
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: a47d99629c4294cef8b308ac17e98f5be3967d9c2663caab2ab633c81d512d2e
|
4
|
+
data.tar.gz: 0c325f17f38543a9a7420a9e1ecf727140ecfdc449853ea434f7643343b710e3
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: e5a49d7d49f0b9f0bc780097620394ecd8d96d62722e746bd34bb9834ba080b767d01527f2417bfb8b8e85c8cda6522621c6b9e0184b0791361343c2fb7b6461
|
7
|
+
data.tar.gz: dbc38700dccecb3e48c18525a836a6b5f2115e6fae5c662e2df7f325e54cd68b82d1940178ab98be41a89fda98a97dc502a18c6038cbd39c2c2e86a5d45890d5
|
checksums.yaml.gz.sig
CHANGED
Binary file
|
data/GettingStarted.md
ADDED
@@ -0,0 +1,234 @@
|
|
1
|
+
# Presentability - Getting Started
|
2
|
+
|
3
|
+
To set up your own presentation layer, there are three steps:
|
4
|
+
|
5
|
+
- Set up a Module as a presenter collection.
|
6
|
+
- Declare one or more _presenters_ within the collection.
|
7
|
+
- Use the collection to present entities from a service or other limited interface.
|
8
|
+
|
9
|
+
For the purposes of this document, we'll pretend we're responsible for creating a JSON web service for Acme Widgets, Inc. We've declared all of our company's code inside the `Acme` namespace. We're using a generic Sinatra-like web framework that lets you declare endpoints like so:
|
10
|
+
|
11
|
+
```ruby
|
12
|
+
get '/status_check' do |parameters|
|
13
|
+
return { status: 'success' }.to_json
|
14
|
+
end
|
15
|
+
```
|
16
|
+
|
17
|
+
|
18
|
+
## Create a Presenter Collection
|
19
|
+
|
20
|
+
A _presenter collection_ is just a Module somewhere within your namespace that one can use to access the declared presenters. You designate it as a presenter collection simply by `extend`ing `Presentability`.
|
21
|
+
|
22
|
+
We'll declare ours under `Acme` and call it `Presenters`:
|
23
|
+
|
24
|
+
```ruby
|
25
|
+
require 'presentability'
|
26
|
+
|
27
|
+
require 'acme'
|
28
|
+
|
29
|
+
module Acme::Presenters
|
30
|
+
extend Presentability
|
31
|
+
|
32
|
+
# Presenters will be declared here
|
33
|
+
|
34
|
+
end # module Acme::Presenters
|
35
|
+
```
|
36
|
+
|
37
|
+
Since we haven't declared any presenters, the collection isn't really all that useful yet, so let's declare some.
|
38
|
+
|
39
|
+
|
40
|
+
## Declare Presenters
|
41
|
+
|
42
|
+
A _presenter_ is an object that is responsible for constructing a _representation_ of another object. There are a number of reasons to use a presenter:
|
43
|
+
|
44
|
+
- Security: avoid exposing sensitive data from your domain objects to the public, e.g., passwords, internal prices, numeric IDs, etc.
|
45
|
+
- Transform: normalize and flatten complex objects to some standard form, e.g., convert `Time` object timestamps to RFC 2822 form.
|
46
|
+
- Consistency: change your model layer independently of your service's entities, e.g., adding a new column to a model doesn't automatically expose it in the service layer.
|
47
|
+
|
48
|
+
The _representation_ is just a simple object that serves as an intermediate form for the transformed object until it is ultimately encoded. The default _representation_ is an empty `Hash`, but you can customize it to suit your needs.
|
49
|
+
|
50
|
+
To declare a presenter, we'll call the `presenter_for` method on the presenter collection module, and then call `expose` or `expose_collection` for each attribute that should be exposed.
|
51
|
+
|
52
|
+
The first argument to `presenter_for` is the type of object the presenter is for, which can be specified in a couple of different ways. The easiest is to just pass in the class itself. The domain class the Acme service is built around is the Widget, so let's declare a presenter for it:
|
53
|
+
|
54
|
+
```ruby
|
55
|
+
require 'acme/widget'
|
56
|
+
|
57
|
+
module Acme::Presenters
|
58
|
+
extend Presentability
|
59
|
+
|
60
|
+
presenter_for Acme::Widget do
|
61
|
+
expose :name
|
62
|
+
end
|
63
|
+
|
64
|
+
end # module Acme::Presenters
|
65
|
+
```
|
66
|
+
|
67
|
+
To present an object, call `.present` on the collection module with the object to be presented, and it will return a representation that is a `Hash` with a single `:name` key-value pair:
|
68
|
+
|
69
|
+
```ruby
|
70
|
+
widget = Acme::Widget.where( name: 'The Red One' )
|
71
|
+
Acme::Presenters.present( widget )
|
72
|
+
# => { name: "The Red One" }
|
73
|
+
```
|
74
|
+
|
75
|
+
If we want to add a `sku` field to all widgets served by our service, we just add another exposure:
|
76
|
+
|
77
|
+
```ruby
|
78
|
+
expose :sku
|
79
|
+
```
|
80
|
+
|
81
|
+
```ruby
|
82
|
+
widget = Acme::Widget.where( name: 'The Red One' )
|
83
|
+
Acme::Presenters.present( widget )
|
84
|
+
# => { name: "The Red One", sku: 'DGG-17044-0822' }
|
85
|
+
```
|
86
|
+
|
87
|
+
### Overriding Exposures
|
88
|
+
|
89
|
+
Sometime you want to alter the value that appears for a particular field. Say, for example, that the SKU that we exposed in our Widget presenter has an internal-only suffix in the form: `-xxxx` that we'd like to avoid exposing in a public-facing service. We can accomplish this by adding a block to it that alters the field from the model. Inside this block, the original object can be accessed via the `subject` method, so we can call the original `#sku` method and truncate it:
|
90
|
+
|
91
|
+
```ruby
|
92
|
+
expose :sku do
|
93
|
+
original = self.subject.sku
|
94
|
+
return original.sub(/-\d{4}/, '')
|
95
|
+
end
|
96
|
+
```
|
97
|
+
|
98
|
+
Now the last part of the SKU will be removed in the representation:
|
99
|
+
|
100
|
+
```ruby
|
101
|
+
widget = Acme::Widget.where( name: 'The Red One' )
|
102
|
+
Acme::Presenters.present( widget )
|
103
|
+
# => { name: "The Red One", sku: 'DGG-17044' }
|
104
|
+
```
|
105
|
+
|
106
|
+
### Exposure Options
|
107
|
+
|
108
|
+
You can also pass zero or more options as a keyword Hash when presenting:
|
109
|
+
|
110
|
+
```ruby
|
111
|
+
Acme::Presenters.present( widget, internal: true )
|
112
|
+
```
|
113
|
+
|
114
|
+
There are a few ways options can be used out of the box:
|
115
|
+
|
116
|
+
#### Exposure Aliases
|
117
|
+
|
118
|
+
Sometimes you want the field in the representation to have a different name than the method on the model object:
|
119
|
+
|
120
|
+
```ruby
|
121
|
+
presenter_for Acme::Company do
|
122
|
+
expose :id
|
123
|
+
expose :legal_entity, as: :name
|
124
|
+
expose :icon_url, as: :icon
|
125
|
+
end
|
126
|
+
```
|
127
|
+
|
128
|
+
In the representation, the `#legal_entity` method will be called on the `Company` being presented and the return value associated with the `:name` key, and the same for `#icon_url` and `:icon`:
|
129
|
+
|
130
|
+
```ruby
|
131
|
+
{ id: 4, name: "John's Small Grapes", icon: "grapes-100.png" }
|
132
|
+
```
|
133
|
+
|
134
|
+
#### Conditional Exposure
|
135
|
+
|
136
|
+
You can make an exposure conditional on an option being passed or not:
|
137
|
+
|
138
|
+
```ruby
|
139
|
+
# Don't include the price if presented with `public: true` option set
|
140
|
+
expose :price, unless: :public
|
141
|
+
|
142
|
+
# Only include the settings if presented with `detailed: true` option set
|
143
|
+
expose :settings, if: :detailed
|
144
|
+
```
|
145
|
+
|
146
|
+
#### Collection Exposure
|
147
|
+
|
148
|
+
A common use-case for conditional presentations is when you want an entity in a collection to be a less-detailed version. E.g.,
|
149
|
+
|
150
|
+
```ruby
|
151
|
+
presenter_for Acme::User do
|
152
|
+
expose :id
|
153
|
+
expose :username
|
154
|
+
expose :email
|
155
|
+
|
156
|
+
expose :settings, unless: :in_collection
|
157
|
+
end
|
158
|
+
```
|
159
|
+
|
160
|
+
You can pass `in_collection: true` when you're presenting, but you can also use the `present_collection` convenience method which sets it for you:
|
161
|
+
|
162
|
+
```ruby
|
163
|
+
users = Acme::User.where( :activated ).limit( 20 )
|
164
|
+
Acme::Presenters.present_collection( users )
|
165
|
+
# => [{ id: 1, username: 'fran', email: 'fran@example.com'}, ...]
|
166
|
+
```
|
167
|
+
|
168
|
+
#### Custom Options
|
169
|
+
|
170
|
+
You also have access to the presenter options (via the `#options` method) in a overridden exposure block. With this you can build your own presentation logic:
|
171
|
+
|
172
|
+
```ruby
|
173
|
+
presenter_for Acme::Widget do
|
174
|
+
expose :name
|
175
|
+
expose :sku
|
176
|
+
|
177
|
+
expose :scancode do
|
178
|
+
self.subject.make_scancode( self.options[:scantype] )
|
179
|
+
end
|
180
|
+
end
|
181
|
+
|
182
|
+
# In your service:
|
183
|
+
widget = Acme::Widget[5]
|
184
|
+
Acme::Presenters.present( widget, scantype: :qr )
|
185
|
+
# { name: "Duck Quackers", sku: 'HBG-121-0424', scancode: '<qrcode data>'}
|
186
|
+
```
|
187
|
+
|
188
|
+
|
189
|
+
## Declare Serializers
|
190
|
+
|
191
|
+
Oftentimes your model objects include values which are themselves not inherently serializable to your representation format. To help with this, you can also declare a "serializer" for one or more classes in your collection using the `.serializer_for` method:
|
192
|
+
|
193
|
+
```ruby
|
194
|
+
require 'time' # for Time#rfc2822
|
195
|
+
|
196
|
+
module Acme::Presenters
|
197
|
+
extend Presentability
|
198
|
+
|
199
|
+
serializer_for :IPAddr, :to_s
|
200
|
+
serializer_for Time, :rfc2822
|
201
|
+
serializer_for Set, :to_a
|
202
|
+
|
203
|
+
end # module Acme::Presenters
|
204
|
+
```
|
205
|
+
|
206
|
+
Now when one of your models includes any of the given types, the corresponding method will be called on it and the result used as the value instead.
|
207
|
+
|
208
|
+
## Use the Roda Plugin
|
209
|
+
|
210
|
+
If you're using the excellent [Roda](https://roda.jeremyevans.net/) web framework, `Presentability` includes a plugin for using it in your Roda application. To enable it, in your app just `require` your collection and enable the plugin. That will enable you to use `#present` and `#present_collection` in your routes:
|
211
|
+
|
212
|
+
```ruby
|
213
|
+
require 'roda'
|
214
|
+
require 'acme/presenters'
|
215
|
+
|
216
|
+
class Acme::WebService < Roda
|
217
|
+
|
218
|
+
plugin :presenters, collection: Acme::Presenters
|
219
|
+
plugin :json
|
220
|
+
|
221
|
+
route do |r|
|
222
|
+
r.on "users" do
|
223
|
+
r.is do
|
224
|
+
# GET /users
|
225
|
+
r.get do
|
226
|
+
users = Acme::User.where( :activated )
|
227
|
+
present_collection( users.all )
|
228
|
+
end
|
229
|
+
end
|
230
|
+
end
|
231
|
+
end
|
232
|
+
end # class Acme::WebService
|
233
|
+
```
|
234
|
+
|
data/History.md
CHANGED
@@ -1,6 +1,24 @@
|
|
1
1
|
# Release History for presentability
|
2
2
|
|
3
3
|
---
|
4
|
+
|
5
|
+
## v0.6.0 [2024-04-29] Michael Granger <ged@faeriemud.org>
|
6
|
+
|
7
|
+
Improvements:
|
8
|
+
|
9
|
+
- Added custom serialization, and better collection handling
|
10
|
+
- Added a Roda plugin
|
11
|
+
- Improved documentation
|
12
|
+
|
13
|
+
|
14
|
+
## v0.5.0 [2023-11-06] Michael Granger <ged@faeriemud.org>
|
15
|
+
|
16
|
+
Improvements:
|
17
|
+
|
18
|
+
- Add recursive presentation
|
19
|
+
- Allow presenters to be defined for objects with no instance variables
|
20
|
+
|
21
|
+
|
4
22
|
## v0.4.0 [2023-02-02] Michael Granger <ged@faeriemud.org>
|
5
23
|
|
6
24
|
Improvements:
|
data/README.md
CHANGED
@@ -56,6 +56,8 @@ your data objects that return a limited representation of their subjects.
|
|
56
56
|
More details can be found in the docs for the Presentability module, and in
|
57
57
|
Presentability::Presenter.
|
58
58
|
|
59
|
+
See GettingStarted to get started with your own presenters.
|
60
|
+
|
59
61
|
|
60
62
|
## Prerequisites
|
61
63
|
|
@@ -88,7 +90,7 @@ This will install dependencies, and do any other necessary setup for development
|
|
88
90
|
|
89
91
|
## License
|
90
92
|
|
91
|
-
Copyright (c) 2022, Michael Granger
|
93
|
+
Copyright (c) 2022-2024, Michael Granger
|
92
94
|
All rights reserved.
|
93
95
|
|
94
96
|
Redistribution and use in source and binary forms, with or without
|
@@ -4,7 +4,119 @@ require 'loggability'
|
|
4
4
|
|
5
5
|
require 'presentability' unless defined?( Presentability )
|
6
6
|
|
7
|
-
#
|
7
|
+
#
|
8
|
+
# A presenter (facade) base class.
|
9
|
+
#
|
10
|
+
#
|
11
|
+
# ### Declaring Presenters
|
12
|
+
#
|
13
|
+
# When you declare a presenter in a Presentability collection, the result is a
|
14
|
+
# subclass of Presentability::Presenter. The main way of defining a Presenter's
|
15
|
+
# functionality is via the ::expose method, which marks an attribute of the underlying
|
16
|
+
# entity object (the "subject") for exposure.
|
17
|
+
#
|
18
|
+
# class MyPresenter < Presentability::Presenter
|
19
|
+
# expose :name
|
20
|
+
# end
|
21
|
+
#
|
22
|
+
# # Assuming `entity_object' has a "name" attribute...
|
23
|
+
# presenter = MyPresenter.new( entity_object )
|
24
|
+
# presenter.apply
|
25
|
+
# # => { :name => "entity name" }
|
26
|
+
#
|
27
|
+
#
|
28
|
+
# ### Presenter Collections
|
29
|
+
#
|
30
|
+
# Setting up classes manually like this is one option, but Presentability also lets you
|
31
|
+
# set them up as a collection, which is what further examples will assume for brevity:
|
32
|
+
#
|
33
|
+
# module MyPresenters
|
34
|
+
# extend Presentability
|
35
|
+
#
|
36
|
+
# presenter_for( EntityObject ) do
|
37
|
+
# expose :name
|
38
|
+
# end
|
39
|
+
#
|
40
|
+
# end
|
41
|
+
#
|
42
|
+
#
|
43
|
+
# ### Complex Exposures
|
44
|
+
#
|
45
|
+
# Sometimes you want to do more than just use the presented entity's values as-is.
|
46
|
+
# There are a number of ways to do this.
|
47
|
+
#
|
48
|
+
# The first of these is to provide a block when exposing an attribute. The subject
|
49
|
+
# of the presenter is available to the block via the `subject` method:
|
50
|
+
#
|
51
|
+
# require 'time'
|
52
|
+
#
|
53
|
+
# presenter_for( LogEvent ) do
|
54
|
+
# # Turn Time objects into RFC2822-formatted time strings
|
55
|
+
# expose :timestamp do
|
56
|
+
# self.subject.timestamp.rfc2822
|
57
|
+
# end
|
58
|
+
#
|
59
|
+
# end
|
60
|
+
#
|
61
|
+
# You can also declare the exposure using a regular method with the same name:
|
62
|
+
#
|
63
|
+
# require 'time'
|
64
|
+
#
|
65
|
+
# presenter_for( LogEvent ) do
|
66
|
+
# # Turn Time objects into RFC2822-formatted time strings
|
67
|
+
# expose :timestamp
|
68
|
+
#
|
69
|
+
# def timestamp
|
70
|
+
# return self.subject.timestamp.rfc2822
|
71
|
+
# end
|
72
|
+
#
|
73
|
+
# end
|
74
|
+
#
|
75
|
+
# This can be used to add presence checks:
|
76
|
+
#
|
77
|
+
# require 'time'
|
78
|
+
#
|
79
|
+
# presenter_for( LogEvent ) do
|
80
|
+
# # Require that presented entities have an `id` attribute
|
81
|
+
# expose :id do
|
82
|
+
# id = self.subject.id or raise "no `id' for %p" % [ self.subject ]
|
83
|
+
# raise "`id' for %p is blank!" % [ self.subject ] if id.blank?
|
84
|
+
#
|
85
|
+
# return id
|
86
|
+
# end
|
87
|
+
# end
|
88
|
+
#
|
89
|
+
# or conditional exposures:
|
90
|
+
#
|
91
|
+
# presenter_for( Acme::Product ) do
|
92
|
+
#
|
93
|
+
# # Truncate the long description if presented as part of a collection
|
94
|
+
# expose :detailed_description do
|
95
|
+
# desc = self.subject.detailed_description
|
96
|
+
# if self.options[:in_collection]
|
97
|
+
# return desc[0..15] + '...'
|
98
|
+
# else
|
99
|
+
# return desc
|
100
|
+
# end
|
101
|
+
# end
|
102
|
+
#
|
103
|
+
# end
|
104
|
+
#
|
105
|
+
#
|
106
|
+
# ### Exposure Aliases
|
107
|
+
#
|
108
|
+
# If you want to expose a field but use a different name in the resulting data
|
109
|
+
# structure, you can use the `:as` option in the exposure declaration:
|
110
|
+
#
|
111
|
+
# presenter_for( LogEvent ) do
|
112
|
+
# expose :timestamp, as: :created_at
|
113
|
+
# end
|
114
|
+
#
|
115
|
+
# presenter = MyPresenter.new( log_event )
|
116
|
+
# presenter.apply
|
117
|
+
# # => { :created_at => '2023-02-01 12:34:02.155365 -0800' }
|
118
|
+
#
|
119
|
+
#
|
8
120
|
class Presentability::Presenter
|
9
121
|
extend Loggability
|
10
122
|
|
@@ -57,6 +169,14 @@ class Presentability::Presenter
|
|
57
169
|
end
|
58
170
|
|
59
171
|
|
172
|
+
### Set up an exposure of a collection with the given +name+. This means it will
|
173
|
+
### have the :in_collection option set by default.
|
174
|
+
def self::expose_collection( name, **options, &block )
|
175
|
+
options = options.merge( unless: :in_collection )
|
176
|
+
self.expose( name, **options, &block )
|
177
|
+
end
|
178
|
+
|
179
|
+
|
60
180
|
### Generate the body an exposure method that delegates to a method with the
|
61
181
|
### same +name+ on its subject.
|
62
182
|
def self::generate_expose_method( name, **options )
|
@@ -107,12 +227,14 @@ class Presentability::Presenter
|
|
107
227
|
|
108
228
|
|
109
229
|
### Apply the exposures to the subject and return the result.
|
110
|
-
def apply
|
230
|
+
def apply( presenters )
|
111
231
|
result = self.empty_representation
|
112
232
|
|
113
233
|
self.class.exposures.each do |name, exposure_options|
|
114
234
|
next if self.skip_exposure?( name )
|
235
|
+
self.log.debug "Presenting %p" % [ name ]
|
115
236
|
value = self.method( name ).call
|
237
|
+
value = presenters.present( value, **exposure_options )
|
116
238
|
key = exposure_options.key?( :as ) ? exposure_options[:as] : name
|
117
239
|
result[ key.to_sym ] = value
|
118
240
|
end
|
data/lib/presentability.rb
CHANGED
@@ -3,14 +3,118 @@
|
|
3
3
|
require 'loggability'
|
4
4
|
|
5
5
|
|
6
|
-
#
|
6
|
+
#
|
7
|
+
# Facade-based presenter toolkit with minimal assumptions.
|
8
|
+
#
|
9
|
+
# ## Basic Usage
|
10
|
+
#
|
11
|
+
# Basic usage of Presentability requires two steps: declaring presenters and
|
12
|
+
# then using them.
|
13
|
+
#
|
14
|
+
# ### Declaring Presenters
|
15
|
+
#
|
16
|
+
# Presenters are just regular Ruby classes with some convenience methods for
|
17
|
+
# declaring exposures, but in a lot of cases you'll want to declare them all in
|
18
|
+
# one place. Presentability offers a mixin that implements a simple DSL for
|
19
|
+
# declaring presenters and their associations to entity classes, intended to be
|
20
|
+
# used in a container module:
|
21
|
+
#
|
22
|
+
# require 'presentability'
|
23
|
+
#
|
24
|
+
# module Acme::Presenters
|
25
|
+
# extend Presentability
|
26
|
+
#
|
27
|
+
# presenter_for( Acme::Widget ) do
|
28
|
+
# expose :sku
|
29
|
+
# expose :name
|
30
|
+
# expose :unit_price
|
31
|
+
# end
|
32
|
+
#
|
33
|
+
# end
|
34
|
+
#
|
35
|
+
# The block of `presenter_for` is evaluated in the context of a new Presenter
|
36
|
+
# class, so refer to that documentation for what's possible there.
|
37
|
+
#
|
38
|
+
# Sometimes you can't (or don't want to) have to load the entity class to
|
39
|
+
# declare a presenter for it, so you can also declare it using the class's name:
|
40
|
+
#
|
41
|
+
# presenter_for( 'Acme::Widget' ) do
|
42
|
+
# expose :sku
|
43
|
+
# expose :name
|
44
|
+
# expose :unit_price
|
45
|
+
# end
|
46
|
+
#
|
47
|
+
#
|
48
|
+
# ### Using Presenters
|
49
|
+
#
|
50
|
+
# You use presenters by instantiating them with the object they are a facade for
|
51
|
+
# (the "subject"), and then applying it:
|
52
|
+
#
|
53
|
+
# acme_widget = Acme::Widget.new(
|
54
|
+
# sku: "FF-2237H455",
|
55
|
+
# name: "Throbbing Frobnulator",
|
56
|
+
# unit_price: 299,
|
57
|
+
# inventory_count: 301,
|
58
|
+
# wholesale_cost: 39
|
59
|
+
# )
|
60
|
+
# presentation = Acme::Presenters.present( acme_widget )
|
61
|
+
# # => { :sku => "FF-2237H455", :name => "Throbbing Frobnulator", :unit_price => 299 }
|
62
|
+
#
|
63
|
+
# If you want to present a collection of objects as a collection, you can apply
|
64
|
+
# presenters to the collection instead:
|
65
|
+
#
|
66
|
+
# widgets_in_stock = Acme::Widget.where { inventory_count > 0 }
|
67
|
+
# collection_presentation = Acme::Presenters.present_collection( widgets_in_stock )
|
68
|
+
# # => [ {:sku => "FF-2237H455", [...]}, {:sku => "FF-2237H460", [...]}, [...] ]
|
69
|
+
#
|
70
|
+
# The collection can be anything that is `Enumerable`.
|
71
|
+
#
|
72
|
+
#
|
73
|
+
# ### Presentation Options
|
74
|
+
#
|
75
|
+
# Sometimes you want a bit more flexibility in what you present, allowing a single
|
76
|
+
# uniform presenter to be used in multiple use cases. To facilitate this, you can pass
|
77
|
+
# an options keyword hash to `#present`:
|
78
|
+
#
|
79
|
+
# presenter_for( 'Acme::Widget' ) do
|
80
|
+
# expose :sku
|
81
|
+
# expose :name
|
82
|
+
# expose :unit_price
|
83
|
+
#
|
84
|
+
# # Only expose the wholesale cost if presented via an internal API
|
85
|
+
# expose :wholesale_cost, if: :internal_api
|
86
|
+
# end
|
87
|
+
#
|
88
|
+
# acme_widget = Acme::Widget.new(
|
89
|
+
# sku: "FF-2237H455",
|
90
|
+
# name: "Throbbing Frobnulator",
|
91
|
+
# unit_price: 299,
|
92
|
+
# inventory_count: 301,
|
93
|
+
# wholesale_cost: 39
|
94
|
+
# )
|
95
|
+
#
|
96
|
+
# # External API remains unchanged:
|
97
|
+
# presentation = Acme::Presenters.present( acme_widget )
|
98
|
+
# # => { :sku => "FF-2237H455", :name => "Throbbing Frobnulator", :unit_price => 299 }
|
99
|
+
#
|
100
|
+
# # But when run from an internal service:
|
101
|
+
# internal_presentation = Acme::Presenters.present( acme_widget, internal_api: true )
|
102
|
+
# # => { :sku => "FF-2237H455", :name => "Throbbing Frobnulator", :unit_price => 299,
|
103
|
+
# # :wholesale_cost => 39 }
|
104
|
+
#
|
105
|
+
# There are some options that are set for you:
|
106
|
+
#
|
107
|
+
# <dl>
|
108
|
+
# <td><code>:in_collection</code></td>
|
109
|
+
# <dd>Set if the current object is being presented as part of a collection.</dd>
|
110
|
+
# </dl>
|
111
|
+
#
|
7
112
|
module Presentability
|
8
113
|
extend Loggability
|
9
114
|
|
10
115
|
|
11
116
|
# Package version
|
12
|
-
VERSION = '0.
|
13
|
-
|
117
|
+
VERSION = '0.6.0'
|
14
118
|
|
15
119
|
# Automatically load subordinate components
|
16
120
|
autoload :Presenter, 'presentability/presenter'
|
@@ -20,15 +124,29 @@ module Presentability
|
|
20
124
|
log_as :presentability
|
21
125
|
|
22
126
|
|
127
|
+
#
|
128
|
+
# Hooks
|
129
|
+
#
|
130
|
+
|
23
131
|
### Extension hook -- decorate the including +mod+.
|
24
132
|
def self::extended( mod )
|
25
133
|
super
|
26
134
|
mod.singleton_class.attr_accessor :presenters
|
135
|
+
mod.singleton_class.attr_accessor :serializers
|
136
|
+
|
27
137
|
mod.presenters = {}
|
138
|
+
mod.serializers = {
|
139
|
+
Array => mod.method( :serialize_array ),
|
140
|
+
Hash => mod.method( :serialize_hash ),
|
141
|
+
}
|
28
142
|
end
|
29
143
|
|
30
144
|
|
31
145
|
|
146
|
+
#
|
147
|
+
# DSL Methods
|
148
|
+
#
|
149
|
+
|
32
150
|
### Set up a presentation for the given +entity_class+.
|
33
151
|
def presenter_for( entity_class, &block )
|
34
152
|
presenter_class = Class.new( Presentability::Presenter )
|
@@ -38,11 +156,30 @@ module Presentability
|
|
38
156
|
end
|
39
157
|
|
40
158
|
|
159
|
+
### Set up a rule for how to serialize objects of the given +type+ if there is
|
160
|
+
### no presenter declared for it.
|
161
|
+
def serializer_for( type, method )
|
162
|
+
self.serializers[ type ] = method
|
163
|
+
end
|
164
|
+
|
165
|
+
|
166
|
+
#
|
167
|
+
# Presentation Methods
|
168
|
+
#
|
169
|
+
|
41
170
|
### Return a representation of the +object+ by applying a declared presentation.
|
42
171
|
def present( object, **options )
|
43
172
|
representation = self.present_by_class( object, **options ) ||
|
44
|
-
self.present_by_classname( object, **options )
|
45
|
-
|
173
|
+
self.present_by_classname( object, **options ) ||
|
174
|
+
self.serialize( object, **options )
|
175
|
+
|
176
|
+
unless representation
|
177
|
+
if object.instance_variables.empty?
|
178
|
+
return object
|
179
|
+
else
|
180
|
+
raise NoMethodError, "no presenter found for %p" % [ object ], caller( 1 )
|
181
|
+
end
|
182
|
+
end
|
46
183
|
|
47
184
|
return representation
|
48
185
|
end
|
@@ -56,6 +193,37 @@ module Presentability
|
|
56
193
|
end
|
57
194
|
|
58
195
|
|
196
|
+
### Serialize the specified +object+ if a serializer has been declared for it
|
197
|
+
### and return the scalar result.
|
198
|
+
def serialize( object, ** )
|
199
|
+
serializer = self.serializers[ object.class ] ||
|
200
|
+
self.serializers[ object.class.name ] or
|
201
|
+
return nil
|
202
|
+
serializer_proc = serializer.to_proc
|
203
|
+
|
204
|
+
return serializer_proc.call( object )
|
205
|
+
end
|
206
|
+
|
207
|
+
|
208
|
+
### Default serializer for an Array; returns a new array of presented objects.
|
209
|
+
def serialize_array( object )
|
210
|
+
return object.map do |member|
|
211
|
+
self.present( member, in_collection: true )
|
212
|
+
end
|
213
|
+
end
|
214
|
+
|
215
|
+
|
216
|
+
### Default serializer for a Hash; returns a new Hash of presented keys and values.
|
217
|
+
def serialize_hash( object )
|
218
|
+
return object.each_with_object( {} ) do |(key, val), newhash|
|
219
|
+
p_key = self.present( key, in_collection: true )
|
220
|
+
p_val = self.present( val, in_collection: true )
|
221
|
+
|
222
|
+
newhash[ p_key ] = p_val
|
223
|
+
end
|
224
|
+
end
|
225
|
+
|
226
|
+
|
59
227
|
#########
|
60
228
|
protected
|
61
229
|
#########
|
@@ -66,7 +234,7 @@ module Presentability
|
|
66
234
|
presenter_class = self.presenters[ object.class ] or return nil
|
67
235
|
presenter = presenter_class.new( object, **presentation_options )
|
68
236
|
|
69
|
-
return presenter.apply
|
237
|
+
return presenter.apply( self )
|
70
238
|
end
|
71
239
|
|
72
240
|
|
@@ -77,7 +245,7 @@ module Presentability
|
|
77
245
|
presenter_class = self.presenters[ classname ] or return nil
|
78
246
|
presenter = presenter_class.new( object, **presentation_options )
|
79
247
|
|
80
|
-
return presenter.apply
|
248
|
+
return presenter.apply( self )
|
81
249
|
end
|
82
250
|
|
83
251
|
end # module Presentability
|