presentability 0.3.0 → 0.5.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 +4 -4
- checksums.yaml.gz.sig +0 -0
- data/History.md +15 -0
- data/Presentability.md +105 -0
- data/Presenter.md +109 -0
- data/lib/presentability/presenter.rb +19 -36
- data/lib/presentability.rb +14 -69
- data/spec/presentability/presenter_spec.rb +25 -7
- data/spec/presentability_spec.rb +143 -5
- data/spec/spec_helper.rb +8 -2
- data.tar.gz.sig +0 -0
- metadata +17 -15
- metadata.gz.sig +0 -0
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: c1802a45773fa3f0e4662926ae1067ae729e81b88201437f7e4bf9705ceac5ec
|
4
|
+
data.tar.gz: c0dedfb32ced019e2105b4434816610598f5f7b560ffab7353144955b9f4a9f3
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: c3c3eb84c0c800517b515defb1b8717f1f940d4c582cb63baf5f7689df9aeb47d63a956f997ba64238301f1db88447cffff972a23faa3fad0026b37b3a0b872b
|
7
|
+
data.tar.gz: 9305b383f0f617186e27411fd153a0aa65b430ae96264c70bc3cc6b9ba99e4d9da77a4e817a9cb2e0870be2c057ed17249a6fad5c1fdd4985edb94b3b4232d3e
|
checksums.yaml.gz.sig
CHANGED
Binary file
|
data/History.md
CHANGED
@@ -1,6 +1,21 @@
|
|
1
1
|
# Release History for presentability
|
2
2
|
|
3
3
|
---
|
4
|
+
## v0.5.0 [2023-11-06] Michael Granger <ged@faeriemud.org>
|
5
|
+
|
6
|
+
Improvements:
|
7
|
+
|
8
|
+
- Add recursive presentation
|
9
|
+
- Allow presenters to be defined for objects with no instance variables
|
10
|
+
|
11
|
+
|
12
|
+
## v0.4.0 [2023-02-02] Michael Granger <ged@faeriemud.org>
|
13
|
+
|
14
|
+
Improvements:
|
15
|
+
|
16
|
+
- Add presentation aliases.
|
17
|
+
|
18
|
+
|
4
19
|
## v0.3.0 [2022-12-16] Michael Granger <ged@faeriemud.org>
|
5
20
|
|
6
21
|
Improvements:
|
data/Presentability.md
ADDED
@@ -0,0 +1,105 @@
|
|
1
|
+
|
2
|
+
Facade-based presenter toolkit with minimal assumptions.
|
3
|
+
|
4
|
+
## Basic Usage
|
5
|
+
|
6
|
+
Basic usage of Presentability requires two steps: declaring presenters and
|
7
|
+
then using them.
|
8
|
+
|
9
|
+
### Declaring Presenters
|
10
|
+
|
11
|
+
Presenters are just regular Ruby classes with some convenience methods for
|
12
|
+
declaring exposures, but in a lot of cases you'll want to declare them all in
|
13
|
+
one place. Presentability offers a mixin that implements a simple DSL for
|
14
|
+
declaring presenters and their associations to entity classes, intended to be
|
15
|
+
used in a container module:
|
16
|
+
|
17
|
+
require 'presentability'
|
18
|
+
|
19
|
+
module Acme::Presenters
|
20
|
+
extend Presentability
|
21
|
+
|
22
|
+
presenter_for( Acme::Widget ) do
|
23
|
+
expose :sku
|
24
|
+
expose :name
|
25
|
+
expose :unit_price
|
26
|
+
end
|
27
|
+
|
28
|
+
end
|
29
|
+
|
30
|
+
The block of `presenter_for` is evaluated in the context of a new Presenter
|
31
|
+
class, so refer to that documentation for what's possible there.
|
32
|
+
|
33
|
+
Sometimes you can't (or don't want to) have to load the entity class to
|
34
|
+
declare a presenter for it, so you can also declare it using the class's name:
|
35
|
+
|
36
|
+
presenter_for( 'Acme::Widget' ) do
|
37
|
+
expose :sku
|
38
|
+
expose :name
|
39
|
+
expose :unit_price
|
40
|
+
end
|
41
|
+
|
42
|
+
|
43
|
+
### Using Presenters
|
44
|
+
|
45
|
+
You use presenters by instantiating them with the object they are a facade for
|
46
|
+
(the "subject"), and then applying it:
|
47
|
+
|
48
|
+
acme_widget = Acme::Widget.new(
|
49
|
+
sku: "FF-2237H455",
|
50
|
+
name: "Throbbing Frobnulator",
|
51
|
+
unit_price: 299,
|
52
|
+
inventory_count: 301,
|
53
|
+
wholesale_cost: 39
|
54
|
+
)
|
55
|
+
presentation = Acme::Presenters.present( acme_widget )
|
56
|
+
# => { :sku => "FF-2237H455", :name => "Throbbing Frobnulator", :unit_price => 299 }
|
57
|
+
|
58
|
+
If you want to present a collection of objects as a collection, you can apply presenters to the collection instead:
|
59
|
+
|
60
|
+
widgets_in_stock = Acme::Widget.where { inventory_count > 0 }
|
61
|
+
collection_presentation = Acme::Presenters.present_collection( widgets_in_stock )
|
62
|
+
# => [ {:sku => "FF-2237H455", [...]}, {:sku => "FF-2237H460", [...]}, [...] ]
|
63
|
+
|
64
|
+
The collection can be anything that is `Enumerable`.
|
65
|
+
|
66
|
+
|
67
|
+
### Presentation Options
|
68
|
+
|
69
|
+
Sometimes you want a bit more flexibility in what you present, allowing a single uniform presenter to be used in multiple use cases. To facilitate this, you can pass an options keyword hash to `#present`:
|
70
|
+
|
71
|
+
presenter_for( 'Acme::Widget' ) do
|
72
|
+
expose :sku
|
73
|
+
expose :name
|
74
|
+
expose :unit_price
|
75
|
+
|
76
|
+
# Only expose the wholesale cost if presented via an internal API
|
77
|
+
expose :wholesale_cost, if: :internal_api
|
78
|
+
end
|
79
|
+
|
80
|
+
acme_widget = Acme::Widget.new(
|
81
|
+
sku: "FF-2237H455",
|
82
|
+
name: "Throbbing Frobnulator",
|
83
|
+
unit_price: 299,
|
84
|
+
inventory_count: 301,
|
85
|
+
wholesale_cost: 39
|
86
|
+
)
|
87
|
+
|
88
|
+
# External API remains unchanged:
|
89
|
+
presentation = Acme::Presenters.present( acme_widget )
|
90
|
+
# => { :sku => "FF-2237H455", :name => "Throbbing Frobnulator", :unit_price => 299 }
|
91
|
+
|
92
|
+
# But when run from an internal service:
|
93
|
+
internal_presentation = Acme::Presenters.present( acme_widget, internal_api: true )
|
94
|
+
# => { :sku => "FF-2237H455", :name => "Throbbing Frobnulator", :unit_price => 299,
|
95
|
+
# :wholesale_cost => 39 }
|
96
|
+
|
97
|
+
There are some options that are set for you:
|
98
|
+
|
99
|
+
<dl>
|
100
|
+
<td><code>:in_collection</code></td>
|
101
|
+
<dd>Set if the current object is being presented as part of a collection.</dd>
|
102
|
+
</dl>
|
103
|
+
|
104
|
+
|
105
|
+
|
data/Presenter.md
ADDED
@@ -0,0 +1,109 @@
|
|
1
|
+
|
2
|
+
A presenter (facade) base class.
|
3
|
+
|
4
|
+
|
5
|
+
### Declaring Presenters
|
6
|
+
|
7
|
+
When you declare a presenter in a Presentability collection, the result is a
|
8
|
+
subclass of Presentability::Presenter. The main way of defining a Presenter's
|
9
|
+
functionality is via the ::expose method, which marks an attribute of the underlying
|
10
|
+
entity object (the "subject") for exposure.
|
11
|
+
|
12
|
+
class MyPresenter < Presentability::Presenter
|
13
|
+
expose :name
|
14
|
+
end
|
15
|
+
|
16
|
+
# Assuming `entity_object' has a "name" attribute...
|
17
|
+
presenter = MyPresenter.new( entity_object )
|
18
|
+
presenter.apply
|
19
|
+
# => { :name => "entity name" }
|
20
|
+
|
21
|
+
|
22
|
+
### Presenter Collections
|
23
|
+
|
24
|
+
Setting up classes manually like this is one option, but Presentability also lets you
|
25
|
+
set them up as a collection, which is what further examples will assume for brevity:
|
26
|
+
|
27
|
+
module MyPresenters
|
28
|
+
extend Presentability
|
29
|
+
|
30
|
+
presenter_for( EntityObject ) do
|
31
|
+
expose :name
|
32
|
+
end
|
33
|
+
|
34
|
+
end
|
35
|
+
|
36
|
+
|
37
|
+
### Complex Exposures
|
38
|
+
|
39
|
+
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.
|
40
|
+
|
41
|
+
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:
|
42
|
+
|
43
|
+
require 'time'
|
44
|
+
|
45
|
+
presenter_for( LogEvent ) do
|
46
|
+
# Turn Time objects into RFC2822-formatted time strings
|
47
|
+
expose :timestamp do
|
48
|
+
self.subject.timestamp.rfc2822
|
49
|
+
end
|
50
|
+
|
51
|
+
end
|
52
|
+
|
53
|
+
You can also declare the exposure using a regular method with the same name:
|
54
|
+
|
55
|
+
require 'time'
|
56
|
+
|
57
|
+
presenter_for( LogEvent ) do
|
58
|
+
# Turn Time objects into RFC2822-formatted time strings
|
59
|
+
expose :timestamp
|
60
|
+
|
61
|
+
def timestamp
|
62
|
+
return self.subject.timestamp.rfc2822
|
63
|
+
end
|
64
|
+
|
65
|
+
end
|
66
|
+
|
67
|
+
This can be used to add presence checks:
|
68
|
+
|
69
|
+
require 'time'
|
70
|
+
|
71
|
+
presenter_for( LogEvent ) do
|
72
|
+
# Require that presented entities have an `id` attribute
|
73
|
+
expose :id do
|
74
|
+
id = self.subject.id or raise "no `id' for %p" % [ self.subject ]
|
75
|
+
raise "`id' for %p is blank!" % [ self.subject ] if id.blank?
|
76
|
+
|
77
|
+
return id
|
78
|
+
end
|
79
|
+
end
|
80
|
+
|
81
|
+
or conditional exposures:
|
82
|
+
|
83
|
+
presenter_for( Acme::Product ) do
|
84
|
+
|
85
|
+
# Truncate the long description if presented as part of a collection
|
86
|
+
expose :detailed_description do
|
87
|
+
desc = self.subject.detailed_description
|
88
|
+
if self.options[:in_collection]
|
89
|
+
return desc[0..15] + '...'
|
90
|
+
else
|
91
|
+
return desc
|
92
|
+
end
|
93
|
+
end
|
94
|
+
|
95
|
+
end
|
96
|
+
|
97
|
+
|
98
|
+
### Exposure Aliases
|
99
|
+
|
100
|
+
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:
|
101
|
+
|
102
|
+
presenter_for( LogEvent ) do
|
103
|
+
expose :timestamp, as: :created_at
|
104
|
+
end
|
105
|
+
|
106
|
+
presenter = MyPresenter.new( log_event )
|
107
|
+
presenter.apply
|
108
|
+
# => { :created_at => '2023-02-01 12:34:02.155365 -0800' }
|
109
|
+
|
@@ -1,43 +1,10 @@
|
|
1
1
|
# -*- ruby -*-
|
2
|
-
# frozen_string_literal: true
|
3
2
|
|
4
3
|
require 'loggability'
|
5
4
|
|
6
5
|
require 'presentability' unless defined?( Presentability )
|
7
6
|
|
8
|
-
#
|
9
|
-
# A presenter (facade) base class.
|
10
|
-
#
|
11
|
-
# When you declare a presenter in a Presentability collection, the result is a
|
12
|
-
# subclass of Presentability::Presenter. The main way of defining a Presenter's
|
13
|
-
# functionality is via the ::expose method, which marks an attribute of the underlying
|
14
|
-
# entity object (the "subject") for exposure.
|
15
|
-
#
|
16
|
-
# ```ruby
|
17
|
-
# class MyPresenter < Presentability::Presenter
|
18
|
-
# expose :name
|
19
|
-
# end
|
20
|
-
#
|
21
|
-
# # Assuming `entity_object' has a "name" attribute...
|
22
|
-
# presenter = MyPresenter.new( entity_object )
|
23
|
-
# presenter.apply
|
24
|
-
# # => { :name => "entity name" }
|
25
|
-
# ```
|
26
|
-
#
|
27
|
-
# Setting up classes like this manually is one option, but Presentability also lets you
|
28
|
-
# set them up as a collection, which is what further examples will assume for brevity:
|
29
|
-
#
|
30
|
-
# ```ruby
|
31
|
-
# module MyPresenters
|
32
|
-
# extend Presentability
|
33
|
-
#
|
34
|
-
# presenter_for( EntityObject ) do
|
35
|
-
# expose :name
|
36
|
-
# end
|
37
|
-
#
|
38
|
-
# end
|
39
|
-
# ```
|
40
|
-
#
|
7
|
+
# :include: Presenter.md
|
41
8
|
class Presentability::Presenter
|
42
9
|
extend Loggability
|
43
10
|
|
@@ -63,6 +30,7 @@ class Presentability::Presenter
|
|
63
30
|
|
64
31
|
|
65
32
|
##
|
33
|
+
# :singleton-method: exposures
|
66
34
|
# The Hash of exposures declared by this class
|
67
35
|
singleton_class.attr_accessor :exposures
|
68
36
|
|
@@ -80,11 +48,23 @@ class Presentability::Presenter
|
|
80
48
|
define_method( name, &method_body )
|
81
49
|
end
|
82
50
|
|
51
|
+
if (exposure_alias = options[:as]) && self.exposures.key?( exposure_alias )
|
52
|
+
raise ScriptError, "alias %p collides with another exposure" % [ exposure_alias ]
|
53
|
+
end
|
54
|
+
|
83
55
|
self.log.debug "Setting up exposure %p, options = %p" % [ name, options ]
|
84
56
|
self.exposures[ name ] = options
|
85
57
|
end
|
86
58
|
|
87
59
|
|
60
|
+
### Set up an exposure of a collection with the given +name+. This means it will
|
61
|
+
### have the :in_collection option set by default.
|
62
|
+
def self::expose_collection( name, **options, &block )
|
63
|
+
options = options.merge( unless: :in_collection )
|
64
|
+
self.expose( name, **options, &block )
|
65
|
+
end
|
66
|
+
|
67
|
+
|
88
68
|
### Generate the body an exposure method that delegates to a method with the
|
89
69
|
### same +name+ on its subject.
|
90
70
|
def self::generate_expose_method( name, **options )
|
@@ -135,13 +115,16 @@ class Presentability::Presenter
|
|
135
115
|
|
136
116
|
|
137
117
|
### Apply the exposures to the subject and return the result.
|
138
|
-
def apply
|
118
|
+
def apply( presenters )
|
139
119
|
result = self.empty_representation
|
140
120
|
|
141
121
|
self.class.exposures.each do |name, exposure_options|
|
142
122
|
next if self.skip_exposure?( name )
|
143
123
|
value = self.method( name ).call
|
144
|
-
|
124
|
+
value = presenters.present( value, **exposure_options ) unless
|
125
|
+
value.is_a?( result.class )
|
126
|
+
key = exposure_options.key?( :as ) ? exposure_options[:as] : name
|
127
|
+
result[ key.to_sym ] = value
|
145
128
|
end
|
146
129
|
|
147
130
|
return result
|
data/lib/presentability.rb
CHANGED
@@ -1,78 +1,15 @@
|
|
1
1
|
# -*- ruby -*-
|
2
|
-
# frozen_string_literal: true
|
3
2
|
|
4
3
|
require 'loggability'
|
5
4
|
|
6
5
|
|
7
|
-
#
|
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
|
-
# ```ruby
|
23
|
-
# require 'presentability'
|
24
|
-
#
|
25
|
-
# module Acme::Presenters
|
26
|
-
# extend Presentability
|
27
|
-
#
|
28
|
-
# presenter_for( Acme::Widget ) do
|
29
|
-
# expose :sku
|
30
|
-
# expose :name
|
31
|
-
# expose :unit_price
|
32
|
-
# end
|
33
|
-
#
|
34
|
-
# end
|
35
|
-
# ```
|
36
|
-
#
|
37
|
-
# The block of `presenter_for` is evaluated in the context of a new Presenter
|
38
|
-
# class, so refer to that documentation for what's possible there.
|
39
|
-
#
|
40
|
-
# Sometimes you can't (or don't want to) have to load the entity class to
|
41
|
-
# declare a presenter for it, so you can also declare it using the class's name:
|
42
|
-
#
|
43
|
-
# ```ruby
|
44
|
-
# presenter_for( 'Acme::Widget' ) do
|
45
|
-
# expose :sku
|
46
|
-
# expose :name
|
47
|
-
# expose :unit_price
|
48
|
-
# end
|
49
|
-
# ```
|
50
|
-
#
|
51
|
-
# ### Using Presenters
|
52
|
-
#
|
53
|
-
# You use presenters by instantiating them with the object they are a facade for
|
54
|
-
# (the "subject"), and then applying it:
|
55
|
-
#
|
56
|
-
# ```ruby
|
57
|
-
# acme_widget = Acme::Widget.new(
|
58
|
-
# sku: "FF-2237H455",
|
59
|
-
# name: "Throbbing Frobnulator",
|
60
|
-
# unit_price: 299,
|
61
|
-
# inventory_count: 301,
|
62
|
-
# wholesale_cost: 39
|
63
|
-
# )
|
64
|
-
# presenter = Acme::Presenters.present( acme_widget )
|
65
|
-
# presenter.apply
|
66
|
-
# # => { :sku => "FF-2237H455", :name => "Throbbing Frobnulator", :unit_price => 299 }
|
67
|
-
# ```
|
68
|
-
#
|
69
|
-
#
|
6
|
+
# :include: Presentability.md
|
70
7
|
module Presentability
|
71
8
|
extend Loggability
|
72
9
|
|
73
10
|
|
74
11
|
# Package version
|
75
|
-
VERSION = '0.
|
12
|
+
VERSION = '0.5.0'
|
76
13
|
|
77
14
|
|
78
15
|
# Automatically load subordinate components
|
@@ -104,8 +41,15 @@ module Presentability
|
|
104
41
|
### Return a representation of the +object+ by applying a declared presentation.
|
105
42
|
def present( object, **options )
|
106
43
|
representation = self.present_by_class( object, **options ) ||
|
107
|
-
self.present_by_classname( object, **options )
|
108
|
-
|
44
|
+
self.present_by_classname( object, **options )
|
45
|
+
|
46
|
+
unless representation
|
47
|
+
if object.instance_variables.empty?
|
48
|
+
return object
|
49
|
+
else
|
50
|
+
raise NoMethodError, "no presenter found for %p" % [ object ]
|
51
|
+
end
|
52
|
+
end
|
109
53
|
|
110
54
|
return representation
|
111
55
|
end
|
@@ -114,6 +58,7 @@ module Presentability
|
|
114
58
|
### Return an Array of all representations of the members of the
|
115
59
|
### +collection+ by applying a declared presentation.
|
116
60
|
def present_collection( collection, **options )
|
61
|
+
options = options.merge( in_collection: true )
|
117
62
|
return collection.map {|object| self.present(object, **options) }
|
118
63
|
end
|
119
64
|
|
@@ -128,7 +73,7 @@ module Presentability
|
|
128
73
|
presenter_class = self.presenters[ object.class ] or return nil
|
129
74
|
presenter = presenter_class.new( object, **presentation_options )
|
130
75
|
|
131
|
-
return presenter.apply
|
76
|
+
return presenter.apply( self )
|
132
77
|
end
|
133
78
|
|
134
79
|
|
@@ -139,7 +84,7 @@ module Presentability
|
|
139
84
|
presenter_class = self.presenters[ classname ] or return nil
|
140
85
|
presenter = presenter_class.new( object, **presentation_options )
|
141
86
|
|
142
|
-
return presenter.apply
|
87
|
+
return presenter.apply( self )
|
143
88
|
end
|
144
89
|
|
145
90
|
end # module Presentability
|
@@ -1,5 +1,4 @@
|
|
1
1
|
# -*- ruby -*-
|
2
|
-
# frozen_string_literal: true
|
3
2
|
|
4
3
|
require_relative '../spec_helper'
|
5
4
|
|
@@ -10,7 +9,18 @@ require 'presentability/presenter'
|
|
10
9
|
RSpec.describe( Presentability::Presenter ) do
|
11
10
|
|
12
11
|
let( :presenter_subject ) do
|
13
|
-
OpenStruct.new(
|
12
|
+
OpenStruct.new(
|
13
|
+
country: 'Philippines',
|
14
|
+
export: 'Copper',
|
15
|
+
flower: 'Sampaguita',
|
16
|
+
cities: ['Quezon City', 'Cagayan de Oro', 'Roxas']
|
17
|
+
)
|
18
|
+
end
|
19
|
+
|
20
|
+
let( :presenters ) do
|
21
|
+
mod = Module.new
|
22
|
+
mod.extend( Presentability )
|
23
|
+
return mod
|
14
24
|
end
|
15
25
|
|
16
26
|
|
@@ -21,14 +31,14 @@ RSpec.describe( Presentability::Presenter ) do
|
|
21
31
|
end
|
22
32
|
|
23
33
|
|
24
|
-
describe "
|
34
|
+
describe "concrete subclass" do
|
25
35
|
|
26
36
|
let( :subclass ) { Class.new(described_class) }
|
27
37
|
|
28
38
|
|
29
39
|
it "can be created with just a subject" do
|
30
40
|
presenter = subclass.new( presenter_subject )
|
31
|
-
expect( presenter.apply ).to eq( {} )
|
41
|
+
expect( presenter.apply(presenters) ).to eq( {} )
|
32
42
|
end
|
33
43
|
|
34
44
|
|
@@ -36,7 +46,7 @@ RSpec.describe( Presentability::Presenter ) do
|
|
36
46
|
subclass.expose( :country )
|
37
47
|
presenter = subclass.new( presenter_subject )
|
38
48
|
|
39
|
-
expect( presenter.apply ).to eq({ country: 'Philippines' })
|
49
|
+
expect( presenter.apply(presenters) ).to eq({ country: 'Philippines' })
|
40
50
|
end
|
41
51
|
|
42
52
|
|
@@ -48,9 +58,9 @@ RSpec.describe( Presentability::Presenter ) do
|
|
48
58
|
financial_presenter = subclass.new( presenter_subject, financial: true )
|
49
59
|
cultural_presenter = subclass.new( presenter_subject, financial: false )
|
50
60
|
|
51
|
-
expect( financial_presenter.apply ).
|
61
|
+
expect( financial_presenter.apply(presenters) ).
|
52
62
|
to eq({ country: 'Philippines', export: 'Copper' })
|
53
|
-
expect( cultural_presenter.apply ).
|
63
|
+
expect( cultural_presenter.apply(presenters) ).
|
54
64
|
to eq({ country: 'Philippines', flower: 'Sampaguita' })
|
55
65
|
end
|
56
66
|
|
@@ -87,6 +97,14 @@ RSpec.describe( Presentability::Presenter ) do
|
|
87
97
|
end
|
88
98
|
|
89
99
|
|
100
|
+
it "can expose an attribute as a collection" do
|
101
|
+
subclass.expose( :country )
|
102
|
+
subclass.expose_collection( :cities )
|
103
|
+
|
104
|
+
expect( subclass.exposures[:cities] ).to include( unless: :in_collection )
|
105
|
+
end
|
106
|
+
|
107
|
+
|
90
108
|
it "has useful #inspect output" do
|
91
109
|
presenter = subclass.new( presenter_subject )
|
92
110
|
expect( presenter.inspect ).to match( /Presentability::Presenter\S+ for /i )
|
data/spec/presentability_spec.rb
CHANGED
@@ -1,5 +1,4 @@
|
|
1
1
|
# -*- ruby -*-
|
2
|
-
# frozen_string_literal: true
|
3
2
|
|
4
3
|
require_relative 'spec_helper'
|
5
4
|
|
@@ -44,6 +43,23 @@ RSpec.describe Presentability do
|
|
44
43
|
end
|
45
44
|
end
|
46
45
|
|
46
|
+
let( :complex_entity_class ) do
|
47
|
+
Class.new do
|
48
|
+
def self::name
|
49
|
+
return 'Acme::Pair'
|
50
|
+
end
|
51
|
+
|
52
|
+
def initialize( user:, entity:, overridden: false, locked: true )
|
53
|
+
@user = user
|
54
|
+
@entity = entity
|
55
|
+
@overridden = overridden
|
56
|
+
@locked = locked
|
57
|
+
end
|
58
|
+
|
59
|
+
attr_accessor :user, :entity, :overridden, :locked
|
60
|
+
end
|
61
|
+
end
|
62
|
+
|
47
63
|
let( :entity_instance ) { entity_class.new }
|
48
64
|
let( :other_entity_instance ) do
|
49
65
|
other_entity_class.new(
|
@@ -53,9 +69,12 @@ RSpec.describe Presentability do
|
|
53
69
|
Faker::Internet.password
|
54
70
|
)
|
55
71
|
end
|
72
|
+
let( :complex_entity_instance ) do
|
73
|
+
complex_entity_class.new( user: other_entity_instance, entity: entity_instance )
|
74
|
+
end
|
56
75
|
|
57
76
|
|
58
|
-
describe "
|
77
|
+
describe "when used to extend a module" do
|
59
78
|
|
60
79
|
let( :extended_module ) do
|
61
80
|
mod = Module.new
|
@@ -163,6 +182,42 @@ RSpec.describe Presentability do
|
|
163
182
|
end
|
164
183
|
|
165
184
|
|
185
|
+
it "doesn't try to present objects with no instance variables by default" do
|
186
|
+
object = 'a string'
|
187
|
+
expect( extended_module.present(object) ).to be( object )
|
188
|
+
|
189
|
+
object = 8
|
190
|
+
expect( extended_module.present(object) ).to be( object )
|
191
|
+
|
192
|
+
object = :a_symbol
|
193
|
+
expect( extended_module.present(object) ).to be( object )
|
194
|
+
|
195
|
+
object = Time.now
|
196
|
+
expect( extended_module.present(object) ).to be( object )
|
197
|
+
|
198
|
+
object = %[an array of strings]
|
199
|
+
expect( extended_module.present(object) ).to be( object )
|
200
|
+
|
201
|
+
object = Object.new
|
202
|
+
expect( extended_module.present(object) ).to be( object )
|
203
|
+
end
|
204
|
+
|
205
|
+
|
206
|
+
it "allows presenters to be defined for objects with no instance variables" do
|
207
|
+
extended_module.presenter_for( Time ) do
|
208
|
+
expose :sec
|
209
|
+
expose :usec
|
210
|
+
end
|
211
|
+
|
212
|
+
object = Time.at( 1699287281.336554 )
|
213
|
+
|
214
|
+
expect( extended_module.present(object) ).to eq({
|
215
|
+
sec: object.sec,
|
216
|
+
usec: object.usec
|
217
|
+
})
|
218
|
+
end
|
219
|
+
|
220
|
+
|
166
221
|
it "errors usefully if asked to present an object it knows nothing about" do
|
167
222
|
expect {
|
168
223
|
extended_module.present( entity_instance )
|
@@ -181,10 +236,38 @@ RSpec.describe Presentability do
|
|
181
236
|
end
|
182
237
|
|
183
238
|
|
239
|
+
it "can alias a field to a different name" do
|
240
|
+
extended_module.presenter_for( entity_class ) do
|
241
|
+
expose :foo, as: :bar
|
242
|
+
end
|
243
|
+
|
244
|
+
expect( extended_module.present(entity_instance) ).to eq({ bar: 1 })
|
245
|
+
end
|
246
|
+
|
247
|
+
|
248
|
+
it "doesn't error when aliasing a field to itself" do
|
249
|
+
extended_module.presenter_for( entity_class ) do
|
250
|
+
expose :foo, as: :foo
|
251
|
+
expose :bar, as: :floom
|
252
|
+
end
|
253
|
+
|
254
|
+
expect( extended_module.present(entity_instance) ).to eq({ foo: 1, floom: 'two' })
|
255
|
+
end
|
256
|
+
|
257
|
+
|
258
|
+
it "raises if an alias clobbers another field" do
|
259
|
+
expect {
|
260
|
+
extended_module.presenter_for( entity_class ) do
|
261
|
+
expose :foo
|
262
|
+
expose :bar, as: :foo
|
263
|
+
end
|
264
|
+
}.to raise_error( ScriptError, /alias :foo collides with another exposure/i )
|
265
|
+
end
|
184
266
|
|
185
|
-
describe "collection handling" do
|
186
267
|
|
187
|
-
|
268
|
+
describe "and used to present a collection" do
|
269
|
+
|
270
|
+
it "handles a homogeneous collection" do
|
188
271
|
extended_module.presenter_for( entity_class ) do
|
189
272
|
expose :foo
|
190
273
|
expose :bar
|
@@ -203,7 +286,7 @@ RSpec.describe Presentability do
|
|
203
286
|
end
|
204
287
|
|
205
288
|
|
206
|
-
it "
|
289
|
+
it "handles a heterogeneous collection" do
|
207
290
|
extended_module.presenter_for( entity_class ) do
|
208
291
|
expose :foo
|
209
292
|
expose :bar
|
@@ -260,6 +343,61 @@ RSpec.describe Presentability do
|
|
260
343
|
end
|
261
344
|
end
|
262
345
|
|
346
|
+
|
347
|
+
it "sets the :in_collection option to allow for eliding attributes" do
|
348
|
+
extended_module.presenter_for( entity_class ) do
|
349
|
+
expose :foo
|
350
|
+
expose :bar, unless: :in_collection
|
351
|
+
expose :baz
|
352
|
+
end
|
353
|
+
|
354
|
+
results = extended_module.present_collection( [entity_instance] )
|
355
|
+
|
356
|
+
expect( results.first ).to include( :foo, :baz )
|
357
|
+
expect( results.first ).not_to include( :bar )
|
358
|
+
|
359
|
+
result = extended_module.present( entity_instance )
|
360
|
+
|
361
|
+
expect( result ).to include( :foo, :bar, :baz )
|
362
|
+
end
|
363
|
+
|
364
|
+
end
|
365
|
+
|
366
|
+
|
367
|
+
describe "and used to present a complex object" do
|
368
|
+
|
369
|
+
it "uses registered presenters for sub-objects" do
|
370
|
+
extended_module.presenter_for( entity_class ) do
|
371
|
+
expose :foo
|
372
|
+
expose :bar
|
373
|
+
end
|
374
|
+
extended_module.presenter_for( other_entity_class ) do
|
375
|
+
expose :firstname
|
376
|
+
expose :lastname
|
377
|
+
expose :email
|
378
|
+
end
|
379
|
+
extended_module.presenter_for( complex_entity_class ) do
|
380
|
+
expose :user
|
381
|
+
expose :entity
|
382
|
+
expose :locked
|
383
|
+
end
|
384
|
+
|
385
|
+
result = extended_module.present( complex_entity_instance )
|
386
|
+
|
387
|
+
expect( result ).to eq({
|
388
|
+
user: {
|
389
|
+
firstname: other_entity_instance.firstname,
|
390
|
+
lastname: other_entity_instance.lastname,
|
391
|
+
email: other_entity_instance.email,
|
392
|
+
},
|
393
|
+
entity: {
|
394
|
+
foo: entity_instance.foo,
|
395
|
+
bar: entity_instance.bar
|
396
|
+
},
|
397
|
+
locked: complex_entity_instance.locked,
|
398
|
+
})
|
399
|
+
end
|
400
|
+
|
263
401
|
end
|
264
402
|
|
265
403
|
end
|
data/spec/spec_helper.rb
CHANGED
@@ -1,7 +1,13 @@
|
|
1
1
|
# -*- ruby -*-
|
2
|
-
# frozen_string_literal: true
|
3
2
|
|
4
|
-
|
3
|
+
if ENV['COVERAGE']
|
4
|
+
require 'simplecov'
|
5
|
+
SimpleCov.start do
|
6
|
+
add_filter 'spec/'
|
7
|
+
enable_coverage :branch
|
8
|
+
primary_coverage :branch
|
9
|
+
end
|
10
|
+
end
|
5
11
|
|
6
12
|
require 'rspec'
|
7
13
|
require 'i18n'
|
data.tar.gz.sig
CHANGED
Binary file
|
metadata
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: presentability
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 0.
|
4
|
+
version: 0.5.0
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Michael Granger
|
@@ -10,9 +10,9 @@ bindir: bin
|
|
10
10
|
cert_chain:
|
11
11
|
- |
|
12
12
|
-----BEGIN CERTIFICATE-----
|
13
|
-
MIID+
|
14
|
-
|
15
|
-
|
13
|
+
MIID+DCCAmCgAwIBAgIBBTANBgkqhkiG9w0BAQsFADAiMSAwHgYDVQQDDBdnZWQv
|
14
|
+
REM9RmFlcmllTVVEL0RDPW9yZzAeFw0yMzAxMTYxNzE2MDlaFw0yNDAxMTYxNzE2
|
15
|
+
MDlaMCIxIDAeBgNVBAMMF2dlZC9EQz1GYWVyaWVNVUQvREM9b3JnMIIBojANBgkq
|
16
16
|
hkiG9w0BAQEFAAOCAY8AMIIBigKCAYEAvyVhkRzvlEs0fe7145BYLfN6njX9ih5H
|
17
17
|
L60U0p0euIurpv84op9CNKF9tx+1WKwyQvQP7qFGuZxkSUuWcP/sFhDXL1lWUuIl
|
18
18
|
M4uHbGCRmOshDrF4dgnBeOvkHr1fIhPlJm5FO+Vew8tSQmlDsosxLUx+VB7DrVFO
|
@@ -23,17 +23,17 @@ cert_chain:
|
|
23
23
|
ozilJg4aar2okb/RA6VS87o+d7g6LpDDMMQjH4G9OPnJENLdhu8KnPw/ivSVvQw7
|
24
24
|
N2I4L/ZOIe2DIVuYH7aLHfjZDQv/mNgpAgMBAAGjOTA3MAkGA1UdEwQCMAAwCwYD
|
25
25
|
VR0PBAQDAgSwMB0GA1UdDgQWBBRyjf55EbrHagiRLqt5YAd3yb8k4DANBgkqhkiG
|
26
|
-
|
27
|
-
|
28
|
-
|
29
|
-
|
30
|
-
|
31
|
-
|
32
|
-
|
33
|
-
|
34
|
-
|
26
|
+
9w0BAQsFAAOCAYEARYCeUVBWARNKqF0cvNnLJvFf4hdW2+Rtc7NfC5jQvX9a1oom
|
27
|
+
sfVvS96eER/9cbrphu+vc59EELw4zT+RY3/IesnoE7CaX6zIOFmSmG7K61OHsSLR
|
28
|
+
KqMygcWwyuPXT2JG7JsGHuxbzgaRWe29HbSjBbLYxiMH8Zxh4tKutxzKF7jb0Ggq
|
29
|
+
KAf9MH5LwG8IHVIfV5drT14PvgR3tcvmrn1timPyJl+eZ3LNnm9ofOCweuZCq1cy
|
30
|
+
4Q8LV3vP2Cofy9q+az3DHdaUGlmMiZZZqKixDr1KSS9nvh0ZrKMOUL1sWj/IaxrQ
|
31
|
+
RV3y6td14q49t+xnbj00hPlbW7uE2nLJLt2NAoXiE1Nonndz1seB2c6HL79W9fps
|
32
|
+
E/O12pQjCp/aPUZMt8/8tKW31RIy/KP8XO6OTJNWA8A/oNEI0g5p/LmmEtJKWYr1
|
33
|
+
WmEdESlpWhzFECctefIF2lsN9vaOuof57RM77t2otrtcscDtNarIqjZsIyqtDvtL
|
34
|
+
DttITiit0Vwz7bY0
|
35
35
|
-----END CERTIFICATE-----
|
36
|
-
date:
|
36
|
+
date: 2023-11-06 00:00:00.000000000 Z
|
37
37
|
dependencies:
|
38
38
|
- !ruby/object:Gem::Dependency
|
39
39
|
name: loggability
|
@@ -102,6 +102,8 @@ extra_rdoc_files: []
|
|
102
102
|
files:
|
103
103
|
- History.md
|
104
104
|
- LICENSE.txt
|
105
|
+
- Presentability.md
|
106
|
+
- Presenter.md
|
105
107
|
- README.md
|
106
108
|
- lib/presentability.rb
|
107
109
|
- lib/presentability/presenter.rb
|
@@ -132,7 +134,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
|
|
132
134
|
- !ruby/object:Gem::Version
|
133
135
|
version: '0'
|
134
136
|
requirements: []
|
135
|
-
rubygems_version: 3.
|
137
|
+
rubygems_version: 3.4.21
|
136
138
|
signing_key:
|
137
139
|
specification_version: 4
|
138
140
|
summary: Facade-based presenters with minimal assumptions.
|
metadata.gz.sig
CHANGED
Binary file
|