gift_wrap 0.2.0 → 1.0.0

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
  SHA1:
3
- metadata.gz: 37ded86a1243c84591148c1c278126e00eca4c05
4
- data.tar.gz: cc8f73fc149fdc246b96b43fa597d250562d429e
3
+ metadata.gz: bb9527124687cb5c483401325f7327ba25bed6e5
4
+ data.tar.gz: 9b6dd5ef0e5cedefa05af33f2a9db063f47e258e
5
5
  SHA512:
6
- metadata.gz: a1b13e46b2b4c1eae2aa13e6b5d3c386ae4c2efd211ca6b015c3c7e86edebd9e96ab8ba069e0dd51c9edf594ae7c6a7b5b9547ef0b11662b439af1300e65df46
7
- data.tar.gz: 782ab93cb6d43ffdd15faa770ee183a06f94d56a05a0442c2df21adfce77dca24256ef8d4fbe8152a197f138ca40d9c8e3b27719618b3958cbf244bc318b2469
6
+ metadata.gz: 56de5b8b0df06343dd0ecfb8a0704f87f252f8dfdb923262e3f32aca6beb220b8c2fcbf890b9a86b59f90988cfc174ad3ebfc3d288528cc8d7e1873831417953
7
+ data.tar.gz: d047c26b313a8a2fe156ae91626f8f799ee4afdf06d7ecc9840a60b9310730a4c780b04924770254528476d04cb8605e8dc76e2bd0665457b39d3a72d1944b06
data/Gemfile.lock CHANGED
@@ -1,7 +1,7 @@
1
1
  PATH
2
2
  remote: .
3
3
  specs:
4
- gift_wrap (0.2.0)
4
+ gift_wrap (1.0.0)
5
5
 
6
6
  GEM
7
7
  remote: https://rubygems.org/
data/README.md CHANGED
@@ -1 +1,326 @@
1
1
  # GiftWrap
2
+
3
+ A simple Ruby presenter library, for those who enjoy a strong separation of concerns.
4
+
5
+
6
+ ## What does that mean?
7
+
8
+ A presenter is just a decorator that is specifically concerned with the presentation layer. GiftWrap provides a simple way to "wrap" a domain entity (e.g. model) for the purpose of decorating it with said presentational logic.
9
+
10
+ That sort of "view logic" almost never belongs deep in the core code of your domain entities. Yet somehow throwing together classes to house it elsewhere can feel like a chore comprised mostly of boilerplate.
11
+
12
+ GiftWrap removes this feeling while also being lightweight enough that reading the entire source takes only a few minutes. The core module weighs in at **about 70 lines of code and no depedenecies**. Even the optional helpers for those who use ActiveRecord weigh in at only an additional 30 lines of code, give or take.
13
+
14
+ Even better, any model logic which is not relevant to presentation or view code is not accidentally accessible on a class implementing `GiftWrap::Presenter` (e.g. persistence code doesn't leak through to templates).
15
+
16
+ ## Other Options
17
+
18
+ Competition benefits the consumer, and in this case that's you! There is another great gem that does this named [Draper](https://github.com/drapergem/draper), which used to it call itself a "decorator" library, but now focuses on being a tool for "view models". Don't let this indecision in terminology distract from the incredible amount of features and flexibility the project has developed over the last several years.
19
+
20
+ Another project, [Rectify](https://github.com/andypike/rectify), has a presenter library included in it the same goal as GiftWrap. Rectify's presenters take a more Rails-specific approach, and joins this author in criticising Rails' use of the term "view" for what are just templates.
21
+
22
+ In both libraries' cases (barring abuse of certain Draper features), separating a domain entity's multi-faceted presentational needs from any internal or persistence logic is achieved just as as well as by GiftWrap. These libraries are equally useful contenders to consider if you like the extras they provide: a large featureset for Draper, additional abstractions in the case of Rectify.
23
+
24
+
25
+ ## Overview of Use
26
+
27
+ 0. `gem install 'gift_wrap'`
28
+ 1. `include GiftWrap::Presenter` in a PORO, a Plain Old Ruby Object.
29
+ 2. Call `wrapped_as` with some entity/model name.
30
+ 3. Write your own presentational methods in a safe, isolated place.
31
+
32
+ Optionally, you can delegate methods directly with `unwrap_for` if desired, or use `wrap_association` to keep your presenters referencing each other, instead of letting them meddle in associated core domain models.
33
+
34
+
35
+ ## What You Get
36
+
37
+ - A place to isolate all presentational logic related to a particular domain concept or model.
38
+ - An `attributes` method for capturing a user-defined set methods as a Hash.
39
+ - Easy delegation to the underlying model with `unwrap_for`. Use sparingly and with great dicipline.
40
+ - Short-hand to declare unwrapped methods as attributes, via `attribute: true` in the call to `unwrap_for`.
41
+ - The ability to include any methods you define in the usual manner (i.e. `def make_some_noise`) as an attribute.
42
+ - _Optional_ JSON serialization via `ActiveModel::Serializers::JSON`, derived `attributes`.
43
+ - A way to declare an unwrapped method as an "associations" which should be wrapped in their own presenter class.
44
+ - Array-valued associations are built as an array of the association's presenter class.
45
+
46
+ You might even have one model with two distinct presenters that display the model's attributes in a different manner which are particular to their use in your system's presentation layer.
47
+
48
+
49
+
50
+ ## Simple Use
51
+
52
+ Consider a model class representing a map. A `Map` is of a certain type (physical, political, traffic, etc.), has a defined center point, associated units, a legend and possibly some notes. Some maps show roads, while others do not.
53
+
54
+ Here is such a class:
55
+
56
+ ```ruby
57
+ class Map
58
+ attr_reader :type, :center, :units, :legend
59
+ attr_accessor :notes
60
+
61
+ def initialize(type, center, units, legend = :asshole_mapmaker_forgot_legend)
62
+ @type = type
63
+ @center = center
64
+ @units = units
65
+ @notes = ""
66
+ @legend = legend
67
+ end
68
+
69
+ def shows_roads?
70
+ maps_with_roads.include?(type)
71
+ end
72
+
73
+ private
74
+
75
+ def maps_with_roads
76
+ ['road', 'traffic', 'political']
77
+ end
78
+ end
79
+ ```
80
+
81
+ Now consider a minimal presenter which delegates the `type` and `units` methods, and adds a few presentation-specific methods:
82
+
83
+ ```ruby
84
+ class SimpleMapPresenter
85
+ include GiftWrap::Presenter
86
+
87
+ unwrap_for :type
88
+ unwrap_for :units, attribute: true
89
+
90
+ attribute :metric
91
+
92
+ def metric
93
+ metric_map_units.include?(units)
94
+ end
95
+
96
+ def contains_region?(region_name)
97
+ false # Implementation not important
98
+ end
99
+
100
+ private
101
+
102
+ def metric_map_units
103
+ ['m', 'km']
104
+ end
105
+ end
106
+ ```
107
+
108
+ The methods `type` and `units` are delegated via `unwrap_for`, with `units` being declated as an attribute.
109
+
110
+ The additional presentational methods are `contains_region?` and `metric`, with `metric` being declared as an attribute.
111
+
112
+ Thus a call to `SimpleMapPresenter#attributes` would return a Hash with the keys `:units` and `:metric`, and because they are not explicitly delegated or otherwise referenced, the methods `center`, `legend` and `shows_roads?` on a `Map` object are not accessible on the presenter object.
113
+
114
+
115
+ ## Explicit Reference of Wrapped Object
116
+
117
+ In case you would like to internally access the object which your presenter wraps by a domain-appropriate name, the method `wrapped_as` allows for this.
118
+
119
+ If in the above `Map` class we used this, we could refer to the map by name within instance methods of the presenter. For example, exposing a `has_notes?` method without allowing access to the `notes` method on the map itself:
120
+
121
+ ```ruby
122
+ class SimpleMapPresenter
123
+ include GiftWrap::Presenter
124
+
125
+ wrapped_as :map
126
+
127
+ # Previous implementation goes here
128
+
129
+ def has_notes?
130
+ map.notes && map.notes.length > 0
131
+ end
132
+ end
133
+ ```
134
+
135
+ Or better yet, when providing a default message for missing values, you can keep clunky-looking conditional code such as
136
+ ```erb
137
+ <% if map.notes.length > 0 %>
138
+ <%= map.notes %>
139
+ <% else %>
140
+ (No notes provided)
141
+ <% end %>
142
+ ```
143
+ out of your templates entirely! Behold:
144
+
145
+ ```ruby
146
+ class SimpleMapPresenter
147
+ include GiftWrap::Presenter
148
+
149
+ wrapped_as :map
150
+
151
+ # Previous implementation goes here
152
+
153
+ def has_notes?
154
+ map.notes && map.notes.length > 0
155
+ end
156
+
157
+ def notes
158
+ has_notes? ? map.notes : "(No notes provided)"
159
+ end
160
+ end
161
+ ```
162
+
163
+ Then your view template simply becomes
164
+
165
+ ```erb
166
+ <%= map_presenter.notes %>
167
+ ```
168
+
169
+
170
+ ## Associated Objects with their own Presenters
171
+
172
+ Looking at our simple Map class, we've ignored its `legend` attribute entirely. This is likely expressed as an object with behavior of its own:
173
+
174
+ ```ruby
175
+ class Legend
176
+ def initialize(colored_regions, colored_lines)
177
+ @colored_regions = colored_regions
178
+ @colored_lines = colored_lines
179
+ end
180
+
181
+ def region_meaning(color)
182
+ @colored_regions[color]
183
+ end
184
+
185
+ def line_meaning(color)
186
+ @colored_lines[color]
187
+ end
188
+ end
189
+ ```
190
+
191
+ The `Legend` class can be given two hashes which define some of its colors. So for instance, a traffic map might have a legend which is passed colors for land and water regions, and then any colored lines represent traffic congestion:
192
+
193
+ ```ruby
194
+ traffic_map_legend = Legend.new(
195
+ { beige: "land",
196
+ blue: "water"
197
+ },
198
+ { green: "no congestion",
199
+ yellow: "light congestion",
200
+ red: "heavy congestion",
201
+ black: "impassable"
202
+ })
203
+ ```
204
+
205
+ And accordingly would have its own presenter when it is used in any presentation or view layer of a project:
206
+
207
+ ```ruby
208
+ class LegendPresenter
209
+ include GiftWrap::Presenter
210
+
211
+ unwrap_for :line_meaning
212
+
213
+ attribute :red_lines
214
+
215
+ def red_lines
216
+ line_meaning(:red)
217
+ end
218
+
219
+ def yellow_lines
220
+ line_meaning(:yellow)
221
+ end
222
+
223
+ def green_lines
224
+ line_meaning(:green)
225
+ end
226
+ ```
227
+
228
+ And an example of its use:
229
+
230
+ ```ruby
231
+ traffic_legend_presenter = LegendPresenter.new(traffic_map_legend)
232
+ traffic_legend_presenter.red_lines
233
+ # => "heavy congestion"
234
+ traffic_legend_presenter.yellow_lines
235
+ # => "light congestion"
236
+ traffic_legend_presenter.green_lines
237
+ # => "no congestion"
238
+ traffic_legend_presenter.black_lines
239
+ # => NoMethodError: undefined method `black_lines'
240
+ ```
241
+
242
+ A presenter which wraps a `Map` object and which wishes to expose its `Legend` object would do well to instead expose an instance of `LegendPresenter`. It is preferable to keep adjecent code working at the same level of abstraction where possible.
243
+
244
+ Slavishly re-implementing a method with `def legend` only to return an instance of `LegendPresenter` seems a bit boilerplate, so GiftWrap has a convenience for this:
245
+
246
+ ```ruby
247
+ class LegendaryMapPresenter
248
+ include GiftWrap::Presenter
249
+
250
+ unwrap_for :type, :units
251
+
252
+ wrap_association :legend, with: LegendPresenter
253
+
254
+ def metric?
255
+ metric_map_units.include?(units)
256
+ end
257
+
258
+ private
259
+
260
+ def metric_map_units
261
+ ['m', 'km']
262
+ end
263
+ end
264
+ ```
265
+
266
+ Unlike the `SimpleMapPresenter`, this version has a `legend` method which performs this wrapping for us by calling `wrap_association` and passing the class `LegendPresenter` in the `:with` keyword.
267
+
268
+ ## Customizing Associated Presenters
269
+
270
+ If the name `legend` was for some reason not desirable, there is no need for the method name exposed on the presenter to be the same of that on the wrapped object. Specifying the association's method name is just another keyword argument in the call to `wrap_association`. If the above class instead had
271
+ ```ruby
272
+ wrap_association :legend, with: LegendPresenter, as: :roflcopter
273
+ ```
274
+ Then the associated `Legend`, wrapped in a `LegendPresenter`, would be accessible via `map_presenter.roflcopter` instead of `map_presenter.legend`.
275
+
276
+ Association Presenters can also be **specified on a per-instance basis**, for flexible modification of presentational logic. So if we were malicious map makers and gave a traffic map legend for which every line color meant "no congestion", we could write such a class:
277
+
278
+ ```ruby
279
+ class MisleadingLegendPresenter
280
+ include GiftWrap::Presenter
281
+
282
+ unwrap_for :line_meaning
283
+
284
+ def red_lines
285
+ "no congestion"
286
+ end
287
+
288
+ def yellow_lines
289
+ "no congestion"
290
+ end
291
+
292
+ def green_lines
293
+ "no congestion"
294
+ end
295
+ end
296
+ ```
297
+
298
+ And build our presenter for the traffic map as before, but override the presenter for the `legend` association. This is accomplished by an `:associations` keyword that simply maps the association name to the presenter which should be used. So given a traffic map stored in a variable named `map_with_legend` which references our previous `traffic_map_legend` as its legend:
299
+
300
+ ```ruby
301
+ map_presenter = LegendaryMapPresenter.new(map_with_legend, associations: {
302
+ legend: MisleadingLegendPresenter
303
+ })
304
+ traffic_legend_presenter = map_presenter.legend
305
+ ```
306
+
307
+ The instance-specific presenter will take effect and `traffic_legend_presenter` will act quite differently than before:
308
+
309
+ ```ruby
310
+ traffic_legend_presenter.red_lines
311
+ # => "no congestion"
312
+ traffic_legend_presenter.yellow_lines
313
+ # => "no congestion"
314
+ traffic_legend_presenter.green_lines
315
+ # => "no congestion"
316
+ ```
317
+
318
+
319
+ ## JSON Serialization
320
+
321
+ **(Implemented, Example Docs Coming Soon)**
322
+
323
+
324
+ ## ActiveRecord Convenience Module
325
+
326
+ **(Implemented, Example Docs Coming Soon)**
@@ -112,11 +112,15 @@ module GiftWrap
112
112
  define_method(as) do
113
113
  presenter_class = wrapped_association_presenter(as)
114
114
  associated = @wrapped_object.send(association)
115
- if associated.respond_to?(:each)
116
- associated.map { |assoc| presenter_class.new(assoc, **options) }
117
- else
118
- presenter_class.new(associated, **options)
119
- end
115
+ memoized_within = "@#{as}"
116
+ instance_variable_get(memoized_within) ||
117
+ instance_variable_set(memoized_within,
118
+ if associated.respond_to?(:each)
119
+ associated.map { |assoc| presenter_class.new(assoc, **options) }
120
+ else
121
+ presenter_class.new(associated, **options)
122
+ end
123
+ )
120
124
  end
121
125
  end
122
126
 
@@ -1,3 +1,3 @@
1
1
  module GiftWrap
2
- VERSION = '0.2.0'
2
+ VERSION = '1.0.0'
3
3
  end
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: gift_wrap
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.2.0
4
+ version: 1.0.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Paul Kwiatkowski
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2016-06-27 00:00:00.000000000 Z
11
+ date: 2016-08-25 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: activemodel
@@ -157,7 +157,7 @@ files:
157
157
  - test/unit/active_record_presenter_test.rb
158
158
  - test/unit/configuration_test.rb
159
159
  - test/unit/presenter_test.rb
160
- homepage: https://github.com/swifthand/adalog
160
+ homepage: https://github.com/swifthand/gift_wrap
161
161
  licenses:
162
162
  - Revised BSD, see LICENSE.md
163
163
  metadata: {}