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

Sign up to get free protection for your applications and to get access to all the features.
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