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 +4 -4
- data/lib/presentability/presenter.rb +55 -6
- data/lib/presentability.rb +18 -5
- data/spec/presentability/presenter_spec.rb +98 -0
- data/spec/presentability_spec.rb +64 -3
- metadata +17 -2
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 804aef5ae0367b75c4182c98315ad2791419fde213551ea9a5d9e860849b3e44
|
4
|
+
data.tar.gz: be7e218aafbbeb7f743d612da6498ab0b8a841594ca704fe342c2df2751bf1ec
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
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,
|
76
|
-
|
77
|
-
|
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
|
-
|
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
|
+
|
data/lib/presentability.rb
CHANGED
@@ -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.
|
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
|
+
|
data/spec/presentability_spec.rb
CHANGED
@@ -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
|
-
|
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
|
-
|
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(
|
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.
|
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-
|
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
|