presentability 0.1.0.pre.20220807214309 → 0.1.0.pre.20220808082516

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: 9cb368623de9f8e87b526b7ec806fb7469230df1afc2092d1e83c5c00a134140
4
- data.tar.gz: a2db6a19a38d9bea3113807df66eea6f5a4ba198647291f8c2269cf5c09e2182
3
+ metadata.gz: 804aef5ae0367b75c4182c98315ad2791419fde213551ea9a5d9e860849b3e44
4
+ data.tar.gz: be7e218aafbbeb7f743d612da6498ab0b8a841594ca704fe342c2df2751bf1ec
5
5
  SHA512:
6
- metadata.gz: f4a042bd7a2abdb74182c429ab3889a2a9e50765f67676be07c2697a22156e364b48c3e182c9260515c96e99d73b585b4893a332720e63333187a66c976110cc
7
- data.tar.gz: dd0e2b57e05dc7f1c568ada5e6d5c94dac338f176c233a0d4b1509d9c455bc6d07a1274cfda2b7fe48cb30b4619e9ef6c21f34dc8f76781718c2ed6d5b110ab5
6
+ metadata.gz: e461283d2b5c43af73319f41277b870b57372329a3194c2b37609a4a537493dfe12bf59be44d9da83a491957eb7f965430e0c670697d1058abb43cd30d4c2564
7
+ data.tar.gz: ba92018b64fe673aa858d1fc47381d43c11a0f5fe27bb640a9a44bf5f9f194fc2791f0faad3358fb44b9334dfe587bb9a49f461ad8fed6c11eb8d4cf96920635
@@ -37,7 +37,7 @@ class Presentability::Presenter
37
37
 
38
38
  ### Set up an exposure that will delegate to the attribute of the subject with
39
39
  ### the given +name+.
40
- def self::expose( name, options={} )
40
+ def self::expose( name, **options )
41
41
  options = DEFAULT_EXPOSURE_OPTIONS.merge( options )
42
42
 
43
43
  self.log.debug "Setting up exposure %p %p" % [ name, options ]
@@ -46,8 +46,9 @@ class Presentability::Presenter
46
46
 
47
47
 
48
48
  ### Create a new Presenter for the given +subject+.
49
- def initialize( subject )
49
+ def initialize( subject, options={} )
50
50
  @subject = subject
51
+ @options = options
51
52
  end
52
53
 
53
54
 
@@ -60,6 +61,10 @@ class Presentability::Presenter
60
61
  # building the representation.
61
62
  attr_reader :subject
62
63
 
64
+ ##
65
+ # The presentation options
66
+ attr_reader :options
67
+
63
68
 
64
69
  ### Return a new instance of whatever object type will be used to represent the
65
70
  ### subject.
@@ -72,13 +77,57 @@ class Presentability::Presenter
72
77
  def apply
73
78
  result = self.empty_representation
74
79
 
75
- self.class.exposures.each do |name, opts|
76
- # :TODO: #public_send instead?
77
- value = self.subject.send( name )
80
+ self.class.exposures.each do |name, exposure_options|
81
+ next if self.skip_exposure?( name )
82
+
83
+ value = self.expose_via_delegation( name, exposure_options ) ||
84
+ self.expose_via_presenter_method( name, exposure_options ) or
85
+ raise NoMethodError, "can't expose %p -- no such attribute exists" % [name]
86
+
78
87
  result[ name.to_sym ] = value
79
88
  end
80
89
 
81
90
  return result
82
91
  end
83
92
 
84
- end # class Presentability::Presenter
93
+
94
+ ### Returns +true+ if the exposure with the specified +name+ should be skipped
95
+ ### for the current #subject and #options.
96
+ def skip_exposure?( name )
97
+ exposure_options = self.class.exposures[ name ] or return true
98
+
99
+ return (exposure_options[:if] && !self.options[ exposure_options[:if] ]) ||
100
+ (exposure_options[:unless] && self.options[ exposure_options[:unless] ])
101
+ end
102
+
103
+
104
+ ### Attempt to expose the attribute with the given +name+ via delegation to the
105
+ ### subject's method of the same name. Returns +nil+ if no such method exists.
106
+ def expose_via_delegation( name, options={} )
107
+ self.log.debug "Trying to expose %p via delegation to %p" % [ name, self.subject ]
108
+
109
+ return nil unless self.subject.respond_to?( name )
110
+
111
+ return self.subject.send( name )
112
+ end
113
+
114
+
115
+ ### Attempt to expose the attribute with the given +name+ via a method on the presenter
116
+ ### of the same name. Returns +nil+ if no such method exists.
117
+ def expose_via_presenter_method( name, options={} )
118
+ self.log.debug "Trying to expose %p via presenter method" % [ name ]
119
+
120
+ return nil unless self.respond_to?( name )
121
+
122
+ meth = self.method( name )
123
+ return meth.call
124
+ end
125
+
126
+
127
+ ### Return a human-readable representation of the object suitable for debugging.
128
+ def inspect
129
+ return "#<Presentability::Presenter:%#0x for %p>" % [ self.object_id / 2, self.subject ]
130
+ end
131
+
132
+ end # class Presentability::Presenter
133
+
@@ -33,15 +33,17 @@ module Presentability
33
33
  ### Set up a presentation for the given +entity_class+.
34
34
  def presenter_for( entity_class, &block )
35
35
  presenter_class = Class.new( Presentability::Presenter )
36
- presenter_class.instance_eval( &block )
36
+ presenter_class.module_eval( &block )
37
37
 
38
38
  self.presenters[ entity_class ] = presenter_class
39
39
  end
40
40
 
41
41
 
42
42
  ### Return a representation of the +object+ by applying a declared presentation.
43
- def present( object )
44
- representation = self.present_by_class( object )
43
+ def present( object, **options )
44
+ representation = self.present_by_class( object, **options ) ||
45
+ self.present_by_classname( object, **options ) or
46
+ raise NoMethodError, "no presenter found for %p" % [ object ]
45
47
 
46
48
  return representation
47
49
  end
@@ -49,9 +51,20 @@ module Presentability
49
51
 
50
52
  ### Return a representation of the +object+ by applying a presenter declared for its
51
53
  ### class. Returns +nil+ if no such presenter exists.
52
- def present_by_class( object )
54
+ def present_by_class( object, **presentation_options )
53
55
  presenter_class = self.presenters[ object.class ] or return nil
54
- presenter = presenter_class.new( object )
56
+ presenter = presenter_class.new( object, **presentation_options )
57
+
58
+ return presenter.apply
59
+ end
60
+
61
+
62
+ ### Return a representation of the +object+ by applying a presenter declared for its
63
+ ### class name. Returns +nil+ if no such presenter exists.
64
+ def present_by_classname( object, **presentation_options )
65
+ classname = object.class.name or return nil
66
+ presenter_class = self.presenters[ classname ] or return nil
67
+ presenter = presenter_class.new( object, **presentation_options )
55
68
 
56
69
  return presenter.apply
57
70
  end
@@ -0,0 +1,98 @@
1
+ # -*- ruby -*-
2
+ # frozen_string_literal: true
3
+
4
+ require_relative '../spec_helper'
5
+
6
+ require 'ostruct'
7
+ require 'presentability/presenter'
8
+
9
+
10
+ RSpec.describe( Presentability::Presenter ) do
11
+
12
+ let( :presenter_subject ) do
13
+ OpenStruct.new( country: 'Philippines', export: 'Copper', flower: 'Sampaguita' )
14
+ end
15
+
16
+
17
+ it "can't be instantiated directly" do
18
+ expect {
19
+ described_class.new( presenter_subject )
20
+ }.to raise_error( NoMethodError, /private method `new'/i )
21
+ end
22
+
23
+
24
+ describe "a concrete subclass" do
25
+
26
+ let( :subclass ) { Class.new(described_class) }
27
+
28
+
29
+ it "can be created with just a subject" do
30
+ presenter = subclass.new( presenter_subject )
31
+ expect( presenter.apply ).to eq( {} )
32
+ end
33
+
34
+
35
+ it "can expose an attribute" do
36
+ subclass.expose( :country )
37
+ presenter = subclass.new( presenter_subject )
38
+
39
+ expect( presenter.apply ).to eq({ country: 'Philippines' })
40
+ end
41
+
42
+
43
+ it "can expose attributes conditionally" do
44
+ subclass.expose( :country )
45
+ subclass.expose( :export, if: :financial )
46
+ subclass.expose( :flower, unless: :financial )
47
+
48
+ financial_presenter = subclass.new( presenter_subject, financial: true )
49
+ cultural_presenter = subclass.new( presenter_subject, financial: false )
50
+
51
+ expect( financial_presenter.apply ).
52
+ to eq({ country: 'Philippines', export: 'Copper' })
53
+ expect( cultural_presenter.apply ).
54
+ to eq({ country: 'Philippines', flower: 'Sampaguita' })
55
+ end
56
+
57
+
58
+ it "doesn't skip exposures that are unconditional" do
59
+ subclass.expose( :country )
60
+
61
+ presenter = subclass.new( presenter_subject )
62
+
63
+ expect( presenter.skip_exposure?(:country) ).to be_falsey
64
+ end
65
+
66
+
67
+ it "skips exposures whose conditions are unmet" do
68
+ subclass.expose( :country )
69
+ subclass.expose( :export, if: :financial )
70
+ subclass.expose( :flower, unless: :financial )
71
+
72
+ presenter = subclass.new( presenter_subject, financial: true )
73
+
74
+ expect( presenter.skip_exposure?(:export) ).to be_falsey
75
+ expect( presenter.skip_exposure?(:flower) ).to be_truthy
76
+ end
77
+
78
+
79
+ it "skips exposures that don't exist" do
80
+ subclass.expose( :country )
81
+ subclass.expose( :export, if: :financial )
82
+ subclass.expose( :flower, unless: :financial )
83
+
84
+ presenter = subclass.new( presenter_subject )
85
+
86
+ expect( presenter.skip_exposure?(:bus_schedule) ).to be_truthy
87
+ end
88
+
89
+
90
+ it "has useful #inspect output" do
91
+ presenter = subclass.new( presenter_subject )
92
+ expect( presenter.inspect ).to match( /Presentability::Presenter\S+ for /i )
93
+ end
94
+
95
+ end
96
+
97
+ end
98
+
@@ -11,6 +11,10 @@ RSpec.describe Presentability do
11
11
 
12
12
  let( :entity_class ) do
13
13
  Class.new do
14
+ def self::name
15
+ return 'Acme::Entity'
16
+ end
17
+
14
18
  def initialize
15
19
  @foo = 1
16
20
  @bar = 'two'
@@ -26,22 +30,79 @@ RSpec.describe Presentability do
26
30
 
27
31
  describe "an extended module" do
28
32
 
29
- subject do
33
+ let( :extended_module ) do
30
34
  mod = Module.new
31
35
  mod.extend( described_class )
32
36
  end
33
37
 
34
38
 
35
39
  it "can define a presenter for an explicit class" do
36
- subject.presenter_for( entity_class ) do
40
+ extended_module.presenter_for( entity_class ) do
41
+ expose :foo
42
+ expose :bar
43
+ end
44
+
45
+ expect( extended_module.present(entity_instance) ).to eq({ foo: 1, bar: 'two' })
46
+ end
47
+
48
+
49
+ it "can define a presenter for a class name" do
50
+ extended_module.presenter_for( 'Acme::Entity' ) do
37
51
  expose :foo
38
52
  expose :bar
39
53
  end
40
54
 
41
- expect( subject.present(entity_instance) ).to eq({ foo: 1, bar: 'two' })
55
+ expect( extended_module.present(entity_instance) ).to eq({ foo: 1, bar: 'two' })
42
56
  end
43
57
 
44
58
 
59
+ it "can effect more complex exposures be declaring presenter methods" do
60
+ extended_module.presenter_for( entity_class ) do
61
+ expose :foo
62
+ expose :bar
63
+ expose :id
64
+
65
+ def id
66
+ self.subject.object_id
67
+ end
68
+ end
69
+
70
+ expect( extended_module.present(entity_instance) ).
71
+ to eq({ foo: 1, bar: 'two', id: entity_instance.object_id })
72
+ end
73
+
74
+
75
+ it "can be made conditional on an option being set" do
76
+ extended_module.presenter_for( entity_class ) do
77
+ expose :foo
78
+ expose :bar, if: :include_bar
79
+ end
80
+
81
+ result1 = extended_module.present( entity_instance )
82
+ result2 = extended_module.present( entity_instance, include_bar: true )
83
+
84
+ expect( result1 ).to eq({ foo: 1 })
85
+ expect( result2 ).to eq({ foo: 1, bar: 'two' })
86
+ end
87
+
88
+
89
+ it "errors usefully if asked to present an object it knows nothing about" do
90
+ expect {
91
+ extended_module.present( entity_instance )
92
+ }.to raise_error( NoMethodError, /no presenter found/i )
93
+ end
94
+
95
+
96
+ it "errors usefully if asked to expose an attribute that doesn't exist" do
97
+ extended_module.presenter_for( entity_class ) do
98
+ expose :id
99
+ end
100
+
101
+ expect {
102
+ extended_module.present( entity_instance )
103
+ }.to raise_error( NoMethodError, /can't expose :id -- no such attribute exists/i )
104
+ end
105
+
45
106
  end
46
107
 
47
108
  end
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: presentability
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.1.0.pre.20220807214309
4
+ version: 0.1.0.pre.20220808082516
5
5
  platform: ruby
6
6
  authors:
7
7
  - Michael Granger
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2022-08-07 00:00:00.000000000 Z
11
+ date: 2022-08-08 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: loggability
@@ -38,6 +38,20 @@ dependencies:
38
38
  - - "~>"
39
39
  - !ruby/object:Gem::Version
40
40
  version: '0.19'
41
+ - !ruby/object:Gem::Dependency
42
+ name: rdoc-generator-fivefish
43
+ requirement: !ruby/object:Gem::Requirement
44
+ requirements:
45
+ - - "~>"
46
+ - !ruby/object:Gem::Version
47
+ version: '0.4'
48
+ type: :development
49
+ prerelease: false
50
+ version_requirements: !ruby/object:Gem::Requirement
51
+ requirements:
52
+ - - "~>"
53
+ - !ruby/object:Gem::Version
54
+ version: '0.4'
41
55
  description: Facade-based presenters with minimal assumptions. This library contains
42
56
  utilities for setting up presenters for data classes for things like web services,
43
57
  logging output, etc.
@@ -52,6 +66,7 @@ files:
52
66
  - README.md
53
67
  - lib/presentability.rb
54
68
  - lib/presentability/presenter.rb
69
+ - spec/presentability/presenter_spec.rb
55
70
  - spec/presentability_spec.rb
56
71
  - spec/spec_helper.rb
57
72
  homepage: https://hg.sr.ht/~ged/Presentability