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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: ff4d9944824eaaa9ef6bc0d5e2b02879e83c481f2db9a39fe6c6ebc6f947dd7f
4
- data.tar.gz: 4ccd0e65bdaae2504bf6edad1df4aab25698d5290b740028c5f5dc7c78732302
3
+ metadata.gz: c1802a45773fa3f0e4662926ae1067ae729e81b88201437f7e4bf9705ceac5ec
4
+ data.tar.gz: c0dedfb32ced019e2105b4434816610598f5f7b560ffab7353144955b9f4a9f3
5
5
  SHA512:
6
- metadata.gz: cae40ce72cd721000c47cff537ba2a2ac6635fc27205f79d81548471efda66b499c9aadd8eab97049a457dbd1c784e20ec5cd01805a57c511e1da808517c5ab8
7
- data.tar.gz: 73ccbe66aeeed78447ae7cb91a0bb9cdbe95325d834e6fcfc6baaa4c96103cfb03efa4ef55f3718098ad021f211c17d5365f2e528b0dc5e846570ff6bec3407d
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
- result[ name.to_sym ] = value
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
@@ -1,78 +1,15 @@
1
1
  # -*- ruby -*-
2
- # frozen_string_literal: true
3
2
 
4
3
  require 'loggability'
5
4
 
6
5
 
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
- # ```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.3.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 ) or
108
- raise NoMethodError, "no presenter found for %p" % [ object ]
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( country: 'Philippines', export: 'Copper', flower: 'Sampaguita' )
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 "a concrete subclass" do
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 )
@@ -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 "an extended module" do
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
- it "can present a collection" do
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 "can present a mixed collection" do
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
- require 'simplecov' if ENV['COVERAGE']
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.3.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+DCCAmCgAwIBAgIBBDANBgkqhkiG9w0BAQsFADAiMSAwHgYDVQQDDBdnZWQv
14
- REM9RmFlcmllTVVEL0RDPW9yZzAeFw0yMjAxMDcyMzU4MTRaFw0yMzAxMDcyMzU4
15
- MTRaMCIxIDAeBgNVBAMMF2dlZC9EQz1GYWVyaWVNVUQvREM9b3JnMIIBojANBgkq
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
- 9w0BAQsFAAOCAYEASrm1AbEoxACZ9WXJH3R5axV3U0CA4xaETlL2YT+2nOfVBMQ9
27
- 0ZlkPx6j4ghKJgAIi1TMfDM2JyPJsppQh8tiNccDjWc62UZRY/dq26cMqf/lcI+a
28
- 6YBuEYvzZfearwVs8tHnXtwYV3WSCoCOQaB+nq2lA1O+nkKNl41WOsVbNama5jx3
29
- 8cQtVSEEmZy6jIDJ8c5TmBJ7BQUDEUEWA/A3V42Xyctoj7DvUXWE0lP+X6ypAVSr
30
- lFh3TS64D7NTvxkmg7natUoCvobl6kGl4yMaqE4YRTlfuzhpf91TSOntClqrAOsS
31
- K1s56WndQj3IoBocdY9mQhDZLtLHofSkymoP8btBlj5SsN24TiF0VMSZlctSCYZg
32
- GKyHim/MMlIfGOWsgfioq5jzwmql7W4CDubbb8Lkg70v+hN2E/MnNVAcNE3gyaGc
33
- P5YP5BAbNW+gvd3QHRiWTTuhgHrdDnGdXg93N2M5KHn1ug8BtPLQwlcFwEpKnlLn
34
- btEP+7EplFuoiMfd
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: 2022-12-16 00:00:00.000000000 Z
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.3.7
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