presentability 0.4.0 → 0.6.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 67ea66c2c975ecb5064c9ddb9c0be1a203b0e9e9944dfc3f3821f07e360fc726
4
- data.tar.gz: c13fdaccf960dc87d3feaed73e2a22a96ad805aed83cadbadeeee9cc60e80e49
3
+ metadata.gz: a47d99629c4294cef8b308ac17e98f5be3967d9c2663caab2ab633c81d512d2e
4
+ data.tar.gz: 0c325f17f38543a9a7420a9e1ecf727140ecfdc449853ea434f7643343b710e3
5
5
  SHA512:
6
- metadata.gz: 550a710c7ae589dfc6ebb1ed801508c8e2d347dbd37e1cdd19e3f3699b5b203ad38b7f2dc6a917e71d3e3dd3711e4eefe8c01594a1f4d92f10b120c4fe1e0a82
7
- data.tar.gz: 07d9674d3ad188b6b19923153accddf6175bf61d238d79867b7237a34a10595e066e10f4d37cc702bbcca8b7dcf65fd7df75ad0eb2fa0c4557716be3a370090b
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
- # :include: Presenter.md
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
@@ -3,14 +3,118 @@
3
3
  require 'loggability'
4
4
 
5
5
 
6
- # :include: Presentability.md
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.4.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 ) or
45
- raise NoMethodError, "no presenter found for %p" % [ object ]
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