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 +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
|