presentability 0.1.0.pre.20220808082516 → 0.2.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: 804aef5ae0367b75c4182c98315ad2791419fde213551ea9a5d9e860849b3e44
4
- data.tar.gz: be7e218aafbbeb7f743d612da6498ab0b8a841594ca704fe342c2df2751bf1ec
3
+ metadata.gz: f5b0aae4df8ff0ef130bbcc9704f2f44a9b109dee48cc530db6abdfd0733cbd4
4
+ data.tar.gz: d149966c13d352f89c2f437c8ed251d63520ec33287c920e8ff7c1cd43eeb0e4
5
5
  SHA512:
6
- metadata.gz: e461283d2b5c43af73319f41277b870b57372329a3194c2b37609a4a537493dfe12bf59be44d9da83a491957eb7f965430e0c670697d1058abb43cd30d4c2564
7
- data.tar.gz: ba92018b64fe673aa858d1fc47381d43c11a0f5fe27bb640a9a44bf5f9f194fc2791f0faad3358fb44b9334dfe587bb9a49f461ad8fed6c11eb8d4cf96920635
6
+ metadata.gz: 906172259081cb4c871975c6e8096bfcd7d8a2434903cc8bef9562b9b18c11af889ef1733372d46c94420008baf5821499d19e6708c75224abd962febabc0515
7
+ data.tar.gz: 5d2c5691ee59ba2526755a3a2eab49547d093fa1d2fbedad4163a78ea35b8acc4dc74d20f0e16a90d7b9af3cbedd9a043c68f3b402f8197ea40031984fc4773b
checksums.yaml.gz.sig ADDED
Binary file
data/History.md CHANGED
@@ -1,4 +1,15 @@
1
- ## v0.0.1 [YYYY-MM-DD] Michael Granger <ged@FaerieMUD.org>
1
+ # Release History for presentability
2
+
3
+ ---
4
+
5
+ ## v0.2.0 [2022-12-06] Michael Granger <ged@faeriemud.org>
6
+
7
+ Improvements:
8
+
9
+ - Add collection presentation.
10
+
11
+
12
+ ## v0.1.0 [2022-08-11] Michael Granger <ged@faeriemud.org>
2
13
 
3
14
  Initial release.
4
15
 
data/README.md CHANGED
@@ -22,14 +22,9 @@ services, logging output, etc.
22
22
  It is intended to be dead-simple by default, returning a Hash containing
23
23
  only the attributes you have intentionally exposed from the subject.
24
24
 
25
- # lib/acme/widget.rb
26
- class Acme::Widget
27
- attr_accessor :sku,
28
- :name,
29
- :unit_price,
30
- :internal_cost,
31
- :inventory_count
32
- end
25
+ The most basic usage looks something like this:
26
+
27
+ require 'presentatbility'
33
28
 
34
29
  # lib/acme/presenters.rb
35
30
  module Acme::Presenters
@@ -45,7 +40,7 @@ only the attributes you have intentionally exposed from the subject.
45
40
  # lib/acme/service.rb
46
41
  class Acme::Service < Some::Webservice::Framework
47
42
 
48
- on '/api/widgets/<sku>' do |sku|
43
+ get '/api/widgets/<sku>' do |sku|
49
44
  widget = Acme::Widget.lookup( sku )
50
45
  content_type 'application/json'
51
46
  representation = Acme::Presenters.present( widget )
@@ -5,7 +5,39 @@ require 'loggability'
5
5
 
6
6
  require 'presentability' unless defined?( Presentability )
7
7
 
8
-
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
+ #
9
41
  class Presentability::Presenter
10
42
  extend Loggability
11
43
 
@@ -66,13 +98,6 @@ class Presentability::Presenter
66
98
  attr_reader :options
67
99
 
68
100
 
69
- ### Return a new instance of whatever object type will be used to represent the
70
- ### subject.
71
- def empty_representation
72
- return {}
73
- end
74
-
75
-
76
101
  ### Apply the exposures to the subject and return the result.
77
102
  def apply
78
103
  result = self.empty_representation
@@ -101,6 +126,23 @@ class Presentability::Presenter
101
126
  end
102
127
 
103
128
 
129
+ ### Return a human-readable representation of the object suitable for debugging.
130
+ def inspect
131
+ return "#<Presentability::Presenter:%#0x for %p>" % [ self.object_id / 2, self.subject ]
132
+ end
133
+
134
+
135
+ #########
136
+ protected
137
+ #########
138
+
139
+ ### Return a new instance of whatever object type will be used to represent the
140
+ ### subject.
141
+ def empty_representation
142
+ return {}
143
+ end
144
+
145
+
104
146
  ### Attempt to expose the attribute with the given +name+ via delegation to the
105
147
  ### subject's method of the same name. Returns +nil+ if no such method exists.
106
148
  def expose_via_delegation( name, options={} )
@@ -123,11 +165,5 @@ class Presentability::Presenter
123
165
  return meth.call
124
166
  end
125
167
 
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
168
  end # class Presentability::Presenter
133
169
 
@@ -4,13 +4,75 @@
4
4
  require 'loggability'
5
5
 
6
6
 
7
- # Facade-based presenters with minimal assumptions.
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
+ #
8
70
  module Presentability
9
71
  extend Loggability
10
72
 
11
73
 
12
74
  # Package version
13
- VERSION = '0.0.1'
75
+ VERSION = '0.2.0'
14
76
 
15
77
 
16
78
  # Automatically load subordinate components
@@ -49,6 +111,17 @@ module Presentability
49
111
  end
50
112
 
51
113
 
114
+ ### Return an Array of all representations of the members of the
115
+ ### +collection+ by applying a declared presentation.
116
+ def present_collection( collection, **options )
117
+ return collection.map {|object| self.present(object, **options) }
118
+ end
119
+
120
+
121
+ #########
122
+ protected
123
+ #########
124
+
52
125
  ### Return a representation of the +object+ by applying a presenter declared for its
53
126
  ### class. Returns +nil+ if no such presenter exists.
54
127
  def present_by_class( object, **presentation_options )
@@ -3,6 +3,7 @@
3
3
 
4
4
  require_relative 'spec_helper'
5
5
 
6
+ require 'faker'
6
7
  require 'rspec'
7
8
  require 'presentability'
8
9
 
@@ -15,17 +16,35 @@ RSpec.describe Presentability do
15
16
  return 'Acme::Entity'
16
17
  end
17
18
 
18
- def initialize
19
- @foo = 1
20
- @bar = 'two'
21
- @baz = :three
19
+ def initialize( foo: 1, bar: 'two', baz: :three )
20
+ @foo = foo
21
+ @bar = bar
22
+ @baz = baz
22
23
  end
23
24
 
24
25
  attr_accessor :foo, :bar, :baz
25
26
  end
26
27
  end
27
28
 
29
+ let( :other_entity_class ) do
30
+ Class.new do
31
+ def self::name
32
+ return 'Acme::User'
33
+ end
34
+
35
+ def initialize( firstname, lastname, email, password )
36
+ @firstname = firstname
37
+ @lastname = lastname
38
+ @email = email
39
+ @password = password
40
+ end
41
+
42
+ attr_accessor :firstname, :lastname, :email, :password
43
+ end
44
+ end
45
+
28
46
  let( :entity_instance ) { entity_class.new }
47
+ let( :other_entity_instance ) { entity_class.new }
29
48
 
30
49
 
31
50
  describe "an extended module" do
@@ -103,6 +122,87 @@ RSpec.describe Presentability do
103
122
  }.to raise_error( NoMethodError, /can't expose :id -- no such attribute exists/i )
104
123
  end
105
124
 
125
+
126
+ describe "collection handling" do
127
+
128
+ it "can present a collection" do
129
+ extended_module.presenter_for( entity_class ) do
130
+ expose :foo
131
+ expose :bar
132
+ end
133
+ collection = 5.times.map do
134
+ entity_class.new( foo: rand(100) )
135
+ end
136
+
137
+ results = extended_module.present_collection( collection )
138
+
139
+ expect( results ).to have_attributes( length: 5 )
140
+ results.each do |representation|
141
+ expect( representation ).to include( :foo, :bar )
142
+ expect( representation ).to_not include( :baz )
143
+ end
144
+ end
145
+
146
+
147
+ it "can present a mixed collection" do
148
+ extended_module.presenter_for( entity_class ) do
149
+ expose :foo
150
+ expose :bar
151
+ end
152
+ extended_module.presenter_for( other_entity_class ) do
153
+ expose :firstname
154
+ expose :lastname
155
+ expose :email
156
+ end
157
+
158
+ collection = 5.times.flat_map do
159
+ [
160
+ entity_class.new( foo: rand(100) ),
161
+ other_entity_class.new(
162
+ Faker::Name.first_name,
163
+ Faker::Name.last_name,
164
+ Faker::Internet.email,
165
+ Faker::Internet.password
166
+ )
167
+ ]
168
+ end
169
+
170
+ results = extended_module.present_collection( collection )
171
+
172
+ expect( results ).to have_attributes( length: 10 )
173
+ results.each do |representation|
174
+ expect( representation ).to include( :foo, :bar ).
175
+ or( include(:firstname, :lastname, :email) )
176
+ expect( representation ).to_not include( :baz )
177
+ expect( representation ).to_not include( :password )
178
+ end
179
+ end
180
+
181
+
182
+ it "passes options to the individual presenters" do
183
+ extended_module.presenter_for( entity_class ) do
184
+ expose :foo
185
+ expose :bar, if: :include_bar
186
+ end
187
+ collection = 5.times.map do
188
+ entity_class.new( foo: rand(100) )
189
+ end
190
+
191
+ results = extended_module.present_collection( collection )
192
+ results.each do |representation|
193
+ expect( representation ).to include( :foo )
194
+ expect( representation ).to_not include( :bar, :baz )
195
+ end
196
+
197
+ results = extended_module.present_collection( collection, include_bar: true )
198
+ results.each do |representation|
199
+ expect( representation ).to include( :foo, :bar )
200
+ expect( representation ).to_not include( :baz )
201
+ end
202
+ end
203
+
204
+ end
205
+
106
206
  end
107
207
 
108
208
  end
data/spec/spec_helper.rb CHANGED
@@ -4,6 +4,11 @@
4
4
  require 'simplecov' if ENV['COVERAGE']
5
5
 
6
6
  require 'rspec'
7
+ require 'i18n'
8
+ require 'faker'
9
+
10
+ Faker::Config.locale = 'en'
11
+ I18n.reload!
7
12
 
8
13
  require 'loggability/spechelpers'
9
14
 
data.tar.gz.sig ADDED
Binary file
metadata CHANGED
@@ -1,14 +1,39 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: presentability
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.1.0.pre.20220808082516
4
+ version: 0.2.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Michael Granger
8
8
  autorequire:
9
9
  bindir: bin
10
- cert_chain: []
11
- date: 2022-08-08 00:00:00.000000000 Z
10
+ cert_chain:
11
+ - |
12
+ -----BEGIN CERTIFICATE-----
13
+ MIID+DCCAmCgAwIBAgIBBDANBgkqhkiG9w0BAQsFADAiMSAwHgYDVQQDDBdnZWQv
14
+ REM9RmFlcmllTVVEL0RDPW9yZzAeFw0yMjAxMDcyMzU4MTRaFw0yMzAxMDcyMzU4
15
+ MTRaMCIxIDAeBgNVBAMMF2dlZC9EQz1GYWVyaWVNVUQvREM9b3JnMIIBojANBgkq
16
+ hkiG9w0BAQEFAAOCAY8AMIIBigKCAYEAvyVhkRzvlEs0fe7145BYLfN6njX9ih5H
17
+ L60U0p0euIurpv84op9CNKF9tx+1WKwyQvQP7qFGuZxkSUuWcP/sFhDXL1lWUuIl
18
+ M4uHbGCRmOshDrF4dgnBeOvkHr1fIhPlJm5FO+Vew8tSQmlDsosxLUx+VB7DrVFO
19
+ 5PU2AEbf04GGSrmqADGWXeaslaoRdb1fu/0M5qfPTRn5V39sWD9umuDAF9qqil/x
20
+ Sl6phTvgBrG8GExHbNZpLARd3xrBYLEFsX7RvBn2UPfgsrtvpdXjsHGfpT3IPN+B
21
+ vQ66lts4alKC69TE5cuKasWBm+16A4aEe3XdZBRNmtOu/g81gvwA7fkJHKllJuaI
22
+ dXzdHqq+zbGZVSQ7pRYHYomD0IiDe1DbIouFnPWmagaBnGHwXkDT2bKKP+s2v21m
23
+ ozilJg4aar2okb/RA6VS87o+d7g6LpDDMMQjH4G9OPnJENLdhu8KnPw/ivSVvQw7
24
+ N2I4L/ZOIe2DIVuYH7aLHfjZDQv/mNgpAgMBAAGjOTA3MAkGA1UdEwQCMAAwCwYD
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
35
+ -----END CERTIFICATE-----
36
+ date: 2022-12-06 00:00:00.000000000 Z
12
37
  dependencies:
13
38
  - !ruby/object:Gem::Dependency
14
39
  name: loggability
@@ -24,6 +49,20 @@ dependencies:
24
49
  - - "~>"
25
50
  - !ruby/object:Gem::Version
26
51
  version: '0.18'
52
+ - !ruby/object:Gem::Dependency
53
+ name: faker
54
+ requirement: !ruby/object:Gem::Requirement
55
+ requirements:
56
+ - - "~>"
57
+ - !ruby/object:Gem::Version
58
+ version: '3.0'
59
+ type: :development
60
+ prerelease: false
61
+ version_requirements: !ruby/object:Gem::Requirement
62
+ requirements:
63
+ - - "~>"
64
+ - !ruby/object:Gem::Version
65
+ version: '3.0'
27
66
  - !ruby/object:Gem::Dependency
28
67
  name: rake-deveiate
29
68
  requirement: !ruby/object:Gem::Requirement
@@ -73,11 +112,11 @@ homepage: https://hg.sr.ht/~ged/Presentability
73
112
  licenses:
74
113
  - BSD-3-Clause
75
114
  metadata:
76
- bug_tracker_uri: https://todo.sr.ht/~ged/Presentability
77
- changelog_uri: https://deveiate.org/code/presentability/History_md.html
78
- documentation_uri: https://deveiate.org/code/presentability
79
115
  homepage_uri: https://hg.sr.ht/~ged/Presentability
116
+ documentation_uri: https://deveiate.org/code/presentability
117
+ changelog_uri: https://deveiate.org/code/presentability/History_md.html
80
118
  source_uri: https://hg.sr.ht/~ged/Presentability
119
+ bug_tracker_uri: https://todo.sr.ht/~ged/Presentability
81
120
  post_install_message:
82
121
  rdoc_options: []
83
122
  require_paths:
@@ -89,9 +128,9 @@ required_ruby_version: !ruby/object:Gem::Requirement
89
128
  version: '0'
90
129
  required_rubygems_version: !ruby/object:Gem::Requirement
91
130
  requirements:
92
- - - ">"
131
+ - - ">="
93
132
  - !ruby/object:Gem::Version
94
- version: 1.3.1
133
+ version: '0'
95
134
  requirements: []
96
135
  rubygems_version: 3.3.7
97
136
  signing_key:
metadata.gz.sig ADDED
Binary file