rack-component 0.4.2 → 0.5.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: c5f073b2ec6adad102c5fb898e7071c7531663960f6188c678a1b46c7f564e2f
4
- data.tar.gz: f0bbc1b2c7cee8fca01ec2f7e0d75b0ca260428e24a50ec6f28a9a70e714b6f7
3
+ metadata.gz: f4f080e40de93c62a1a6b1aa9aabf3ee3f2a92fa10823f52c9dade5b075d440f
4
+ data.tar.gz: '08d3866748d7e14320a274f67e3682b67a51448154ecdae5034c2c01ec5a3e3f'
5
5
  SHA512:
6
- metadata.gz: bdfd59e87eccbc6cc0be4a92579813d76195d14e0cfb3620f6ec3a3204730cb8ba35f528d59e260db268bb2a697acb1bb6ccf3c8615fbb52979aa67e15e91858
7
- data.tar.gz: 76ab042215b76839bab82c36ac19682da07050ed0da54d714ffc6e609160ef360962e445b4ef9f71ade86e35ff2dc65668ace167880c92203c4da565f4d09036
6
+ metadata.gz: 6e5c02e8c994ed4fa58e2a62004d248033d796c4f7163217c8a164c127a7ccf26c01a99c3a1fd0415aecff7af2ceae07eec800b2d04ab755da77bb3cd1dd4ab5
7
+ data.tar.gz: 96405e2d4e8b8f9add4d47f1129f035f00a5a9fbf00d6c363c8f056db88d43c962c1c43d367f88cf7557efccfb11386ae760a73b69cab20a795387efd0bab1f2
@@ -24,5 +24,3 @@ Style/TrailingCommaInHashLiteral:
24
24
  Enabled: false
25
25
  StyleGuide: http://relaxed.ruby.style/#styletrailingcommainliteral
26
26
  EnforcedStyleForMultiline: consistent_comma
27
- Style/Documentation:
28
- Enabled: false
@@ -2,5 +2,7 @@
2
2
  language: ruby
3
3
  cache: bundler
4
4
  rvm:
5
- - 2.5.1
5
+ - 2.5.3
6
+ - 2.4
7
+ - 2.3
6
8
  before_install: gem install bundler -v 1.16.5
@@ -0,0 +1,52 @@
1
+ # Changelog
2
+ All notable changes to this project will be documented in this file.
3
+
4
+ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
5
+ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
6
+
7
+ ## 0.5.0
8
+ ### Fixed
9
+ - The `env` argument of the `render` block is now optional, as per standard Ruby
10
+ block behavior.
11
+ ```ruby
12
+ class WorksInThisVersion < Rack::Component
13
+ render do
14
+ 'This component raised an ArgumentError in old versions but works now.'
15
+ end
16
+ end
17
+
18
+ class StillWorks < Rack::Component
19
+ render do |env|
20
+ 'This style still works. Using |keyword:, arguments:| in env is nice.'
21
+ end
22
+ end
23
+ ```
24
+
25
+ ### Added
26
+ - A changelog
27
+ - Templating via [tilt](https://github.com/rtomayko/tilt), with support for
28
+ escaping HTML by default
29
+
30
+ ### Removed
31
+ - Calling `Component.memoized(env)` is no longer supported. Use Sam Saffron's
32
+ [lru_redux](https://github.com/SamSaffron/lru_redux) as an almost drop-in
33
+ replacement, like this:
34
+
35
+ ```ruby
36
+ require 'rack/component'
37
+ require 'lru_redux'
38
+ class MyComponent < Rack::Component
39
+ Cache = LruRedux::ThreadSafeCache.new(100)
40
+
41
+ render do |env|
42
+ Cache.getset(env) { 'this block will render after checking the cache' }
43
+ end
44
+ end
45
+ ```
46
+
47
+ ## 0.4.2 - 2019-01-04
48
+ ### Added
49
+ - `#h` method for escaping HTML inside interpolated strings
50
+
51
+ ## 0.4.1 - 2019-01-02
52
+ - First public, documented release
@@ -21,26 +21,33 @@ GEM
21
21
  thread_safe (~> 0.3, >= 0.3.1)
22
22
  diff-lcs (1.3)
23
23
  equalizer (0.0.11)
24
+ erubi (1.8.0)
25
+ haml (5.0.4)
26
+ temple (>= 0.8.0)
27
+ tilt
24
28
  ice_nine (0.11.2)
25
- jaro_winkler (1.5.1)
29
+ jaro_winkler (1.5.2)
26
30
  kwalify (0.7.2)
27
- method_source (0.9.0)
31
+ liquid (4.0.1)
32
+ method_source (0.9.2)
28
33
  parallel (1.12.1)
29
- parser (2.5.1.2)
34
+ parser (2.5.3.0)
30
35
  ast (~> 2.4.0)
31
36
  powerpack (0.1.2)
32
- pry (0.11.3)
37
+ pry (0.12.2)
33
38
  coderay (~> 1.1.0)
34
39
  method_source (~> 0.9.0)
40
+ psych (3.1.0)
35
41
  rack (2.0.6)
36
42
  rack-test (0.8.3)
37
43
  rack (>= 1.0, < 3)
38
44
  rainbow (3.0.0)
39
45
  rake (10.5.0)
40
- reek (5.2.0)
46
+ reek (5.3.0)
41
47
  codeclimate-engine-rb (~> 0.4.0)
42
48
  kwalify (~> 0.7.0)
43
49
  parser (>= 2.5.0.0, < 2.6, != 2.5.1.1)
50
+ psych (~> 3.1.0)
44
51
  rainbow (>= 2.0, < 4.0)
45
52
  rspec (3.8.0)
46
53
  rspec-core (~> 3.8.0)
@@ -55,18 +62,19 @@ GEM
55
62
  diff-lcs (>= 1.2.0, < 2.0)
56
63
  rspec-support (~> 3.8.0)
57
64
  rspec-support (3.8.0)
58
- rubocop (0.59.2)
65
+ rubocop (0.62.0)
59
66
  jaro_winkler (~> 1.5.1)
60
67
  parallel (~> 1.10)
61
68
  parser (>= 2.5, != 2.5.1.1)
62
69
  powerpack (~> 0.1)
63
70
  rainbow (>= 2.2.2, < 4.0)
64
71
  ruby-progressbar (~> 1.7)
65
- unicode-display_width (~> 1.0, >= 1.0.1)
72
+ unicode-display_width (~> 1.4.0)
66
73
  ruby-progressbar (1.10.0)
74
+ temple (0.8.0)
67
75
  thread_safe (0.3.6)
68
- tilt (2.0.8)
69
- unicode-display_width (1.4.0)
76
+ tilt (2.0.9)
77
+ unicode-display_width (1.4.1)
70
78
  virtus (1.0.5)
71
79
  axiom-types (~> 0.1)
72
80
  coercible (~> 1.0)
@@ -79,7 +87,10 @@ PLATFORMS
79
87
 
80
88
  DEPENDENCIES
81
89
  benchmark-ips (~> 2.7)
82
- bundler (~> 1.16)
90
+ bundler (~> 2)
91
+ erubi (~> 1.8)
92
+ haml (~> 5)
93
+ liquid (~> 4)
83
94
  pry (~> 0.11)
84
95
  rack (~> 2.0.6)
85
96
  rack-component!
@@ -92,4 +103,4 @@ DEPENDENCIES
92
103
  yard (~> 0.9)
93
104
 
94
105
  BUNDLED WITH
95
- 1.17.1
106
+ 2.0.1
data/README.md CHANGED
@@ -13,6 +13,7 @@ gem 'rack-component'
13
13
  ```
14
14
 
15
15
  ## Quickstart with Sinatra
16
+
16
17
  ```ruby
17
18
  # config.ru
18
19
  require 'sinatra'
@@ -20,7 +21,7 @@ require 'rack/component'
20
21
 
21
22
  class Hello < Rack::Component
22
23
  render do |env|
23
- "<h1>Hello, #{h(env[:name])}</h1>"
24
+ "<h1>Hello, #{h env[:name]}</h1>"
24
25
  end
25
26
  end
26
27
 
@@ -31,27 +32,25 @@ end
31
32
  run Sinatra::Application
32
33
  ```
33
34
 
34
- **Note that Rack::Component currently does not escape strings by default**. To
35
- escape strings, you must use the `#h` helper, as in the example above.
36
-
37
- There is an [issue open](https://github.com/chrisfrank/rack-component/issues/4)
38
- to discuss how to enable escaping by default. If you have ideas or opinions, I'd
39
- love to hear about them there.
35
+ **Note that Rack::Component does not escape strings by default**. To escape
36
+ strings, you can either use the `#h` helper like in the example above, or you
37
+ can configure your components to render a template that escapes automatically.
38
+ See the [Recipes](#recipes) section for details.
40
39
 
41
40
  ## Table of Contents
42
41
 
43
42
  * [Getting Started](#getting-started)
44
43
  * [Components as plain functions](#components-as-plain-functions)
45
44
  * [Components as Rack::Components](#components-as-rackcomponents)
46
- * [Components that re-render instantly](#components-that-re-render-instantly)
45
+ * [Components if you hate inheritance](#components-if-you-hate-inheritance)
47
46
  * [Recipes](#recipes)
48
47
  * [Render one component inside another](#render-one-component-inside-another)
49
- * [Memoize an expensive component for one minute](#memoize-an-expensive-component-for-one-minute)
50
- * [Memoize an expensive component until its content changes](#memoize-an-expensive-component-until-its-content-changes)
48
+ * [Render a template that escapes output by default via Tilt](#render-a-template-that-escapes-output-by-default-via-tilt)
51
49
  * [Render an HTML list from an array](#render-an-html-list-from-an-array)
52
50
  * [Render a Rack::Component from a Rails controller](#render-a-rackcomponent-from-a-rails-controller)
53
51
  * [Mount a Rack::Component as a Rack app](#mount-a-rackcomponent-as-a-rack-app)
54
52
  * [Build an entire App out of Rack::Components](#build-an-entire-app-out-of-rackcomponents)
53
+ * [Define `#render` at the instance level instead of via `render do`](#define-render-at-the-instance-level-instead-of-via-render-do)
55
54
  * [API Reference](#api-reference)
56
55
  * [Performance](#performance)
57
56
  * [Compatibility](#compatibility)
@@ -77,57 +76,38 @@ Greeter.call(name: 'Mina') #=> '<h1>Hi, Mina.</h1>'
77
76
 
78
77
  ### Components as Rack::Components
79
78
 
80
- Convert your lambda to a `Rack::Component` when it needs instance methods or
81
- state:
79
+ Upgrade your lambda to a `Rack::Component` when it needs HTML escaping, instance
80
+ methods, or state:
82
81
 
83
82
  ```ruby
84
83
  require 'rack/component'
85
84
  class FormalGreeter < Rack::Component
86
85
  render do |env|
87
- "<h1>Hi, #{title} #{env[:name]}.</h1>"
86
+ "<h1>Hi, #{h title} #{h env[:name]}.</h1>"
88
87
  end
89
88
 
89
+ # +env+ is available in instance methods too
90
90
  def title
91
- # the hash you pass to `call` is available as `env` in instance methods
92
- env[:title] || "President"
91
+ env[:title] || "Queen"
93
92
  end
94
93
  end
95
94
 
96
- FormalGreeter.call(name: 'Macron') #=> "<h1>Hi, President Macron.</h1>"
97
- FormalGreeter.call(name: 'Merkel', title: 'Chancellor') #=> "<h1>Hi, Chancellor Merkel.</h1>"
95
+ FormalGreeter.call(name: 'Franklin') #=> "<h1>Hi, Queen Franklin.</h1>"
96
+ FormalGreeter.call(
97
+ title: 'Captain',
98
+ name: 'Kirk <kirk@starfleet.gov>'
99
+ ) #=> <h1>Hi, Captain Kirk &lt;kirk@starfleet.gov&gt;.</h1>
98
100
  ```
99
101
 
100
- ### Components that re-render instantly
102
+ #### Components if you hate inheritance
101
103
 
102
- Replace `#call` with `#memoized` to make re-renders with the same `env` instant:
104
+ Instead of inheriting from `Rack::Component`, you can `extend` its methods:
103
105
 
104
106
  ```ruby
105
- require 'rack/component'
106
- require 'net/http'
107
- class NetworkGreeter < Rack::Component
108
- render do |env|
109
- "Hi, #{get_job_title_from_api} #{env[:name]}."
110
- end
111
-
112
- def get_job_title_from_api
113
- endpoint = URI("http://api.heads-of-state.gov/")
114
- Net::HTTP.get("#{endpoint}?q=#{env[:name]}")
115
- end
107
+ class SoloComponent
108
+ extend Rack::Component::Methods
109
+ render { "Family is complicated" }
116
110
  end
117
-
118
- NetworkGreeter.memoized(name: 'Macron')
119
- # ...after a slow network call to our fictional Heads Of State API
120
- #=> "Hi, President Macron."
121
-
122
- NetworkGreeter.memoized(name: 'Macron') # subsequent calls with the same env are instant.
123
- #=> "Hi, President Macron."
124
-
125
- NetworkGreeter.memoized(name: 'Merkel')
126
- # ...this env is new, so NetworkGreeter makes another network call
127
- #=> "Hi, Chancellor Merkel."
128
-
129
- NetworkGreeter.memoized(name: 'Merkel') #=> instant! "Hi, Chancellor Merkel."
130
- NetworkGreeter.memoized(name: 'Macron') #=> instant! "Hi, President Macron."
131
111
  ```
132
112
 
133
113
  ## Recipes
@@ -138,7 +118,9 @@ You can nest Rack::Components as if they were [React Children][jsx children] by
138
118
  calling them with a block.
139
119
 
140
120
  ```ruby
141
- Layout.call(title: 'Home') { Content.call }
121
+ Layout.call(title: 'Home') do
122
+ Content.call
123
+ end
142
124
  ```
143
125
 
144
126
  Here's a more fully fleshed example:
@@ -151,11 +133,12 @@ get '/posts/:id' do
151
133
  PostPage.call(id: params[:id])
152
134
  end
153
135
 
154
- # fetch a post from the database and render it inside a layout
136
+ # Fetch a post from the database and render it inside a Layout
155
137
  class PostPage < Rack::Component
156
138
  render do |env|
157
- post = Post.find(id: env[:id])
158
- # Nest a PostContent instance inside a Layout instance, with some arbitrary HTML too
139
+ post = Post.find env[:id]
140
+ # Nest a PostContent instance inside a Layout instance,
141
+ # with some arbitrary HTML too
159
142
  Layout.call(title: post.title) do
160
143
  <<~HTML
161
144
  <main>
@@ -169,80 +152,122 @@ class PostPage < Rack::Component
169
152
  end
170
153
  end
171
154
 
172
- class PostContent < Rack::Component
173
- render do |env|
174
- <<~HTML
175
- <article>
176
- <h1>#{env[:title]}</h1>
177
- #{env[:body]}
178
- </article>
179
- HTML
180
- end
181
- end
182
-
183
155
  class Layout < Rack::Component
184
- render do |env, &children|
185
- # the `&children` param is just a standard ruby block
156
+ # The +render+ macro supports Ruby's keyword arguments, and, like any other
157
+ # Ruby function, can accept a block via the & operator.
158
+ # Here, :title is a required key in +env+, and &child is just a regular Ruby
159
+ # block that could be named anything.
160
+ render do |title:, **, &child|
186
161
  <<~HTML
187
162
  <!DOCTYPE html>
188
163
  <html>
189
164
  <head>
190
- <title>#{env[:title]}</title>
165
+ <title>#{h title}</title>
191
166
  </head>
192
167
  <body>
193
- #{children.call}
168
+ #{child.call}
194
169
  </body>
195
170
  </html>
196
171
  HTML
197
172
  end
198
173
  end
174
+
175
+ class PostContent < Rack::Component
176
+ render do |title:, body:, **|
177
+ <<~HTML
178
+ <article>
179
+ <h1>#{h title}</h1>
180
+ #{h body}
181
+ </article>
182
+ HTML
183
+ end
184
+ end
199
185
  ```
200
186
 
201
- ### Memoize an expensive component for one minute
187
+ ### Render a template that escapes output by default via Tilt
202
188
 
203
- You can use `memoized` as a time-based cache by passing a timestamp to `env`:
189
+ If you add [Tilt][tilt] and `erubi` to your Gemfile, you can use the `render`
190
+ macro with an automatically-escaped template instead of a block.
204
191
 
205
192
  ```ruby
206
- require 'rack/component'
193
+ # Gemfile
194
+ gem 'tilt'
195
+ gem 'erubi'
196
+ gem 'rack-component'
197
+
198
+ # my_component.rb
199
+ class TemplateComponent < Rack::Component
200
+ render erb: <<~ERB
201
+ <h1>Hello, <%= name %></h1>
202
+ ERB
207
203
 
208
- # Render one million posts as JSON
209
- class MillionPosts < Rack::Component
210
- render { |env| Post.limit(1_000_000).to_json }
204
+ def name
205
+ env[:name] || 'Someone'
206
+ end
211
207
  end
212
208
 
213
- MillionPosts.memoized(Time.now.to_i / 60) #=> first call is slow
214
- MillionPosts.memoized(Time.now.to_i / 60) #=> next calls in same minute are quick
209
+ TemplateComponent.call #=> <h1>Hello, Someone</h1>
210
+ TemplateComponent.call(name: 'Spock<>') #=> <h1>Hello, Spock&lt;&gt;</h1>
211
+ ```
212
+
213
+ Rack::Component passes `{ escape_html: true }` to Tilt by default, which enables
214
+ automatic escaping in ERB (via erubi) Haml, and Markdown. To disable automatic
215
+ escaping, or to pass other tilt options, use an `opts: {}` key in `render`:
216
+
217
+ ```ruby
218
+ class OptionsComponent < Rack::Component
219
+ render opts: { escape_html: false, trim: false }, erb: <<~ERB
220
+ <article>
221
+ Hi there, <%= {env[:name] %>
222
+ <%== yield %>
223
+ </article>
224
+ ERB
225
+ end
215
226
  ```
216
227
 
217
- ### Memoize an expensive component until its content changes
228
+ Template components support using the `yield` keyword to render child
229
+ components, but note the double-equals `<%==` in the example above. If your
230
+ component escapes HTML, and you're yielding to a component that renders HTML,
231
+ you probably want to disable escaping via `==`, just for the `<%== yield %>`
232
+ call. This is safe, as long as the component you're yielding to uses escaping.
218
233
 
219
- This recipe will speed things up when your database calls are fast but your
220
- render method is slow:
234
+ Using `erb` as a key for the inline template is a shorthand, which also works
235
+ with `haml` and `markdown`. But you can also specify `engine` and `template`
236
+ explicitly.
221
237
 
222
238
  ```ruby
223
- require 'rack/component'
224
- class PostAnalysis < Rack::Component
225
- render do |env|
226
- <<~HTML
227
- <h1>#{env[:post].title}</h1>
228
- <article>#{env[:post].content}</article>
229
- <aside>#{expensive_natural_language_analysis}</aside>
230
- HTML
231
- end
239
+ require 'haml'
240
+ class HamlComponent < Rack::Component
241
+ # Note the special HEREDOC syntax for inline Haml templates! Without the
242
+ # single-quotes, Ruby will interpret #{strings} before Haml does.
243
+ render engine: 'haml', template: <<~'HAML'
244
+ %h1 Hi #{env[:name]}.
245
+ HAML
246
+ end
247
+ ```
232
248
 
233
- def expensive_natural_language_analysis
234
- FancyNaturalLanguageLibrary.analyze(env[:post].content)
249
+ Using a template instead of raw string interpolation is a safer default, but it
250
+ can make it less convenient to do logic while rendering. Feel free to override
251
+ your Component's `#initialize` method and do logic there:
252
+
253
+ ```ruby
254
+ class EscapedPostView < Rack::Component
255
+ def initialize(env)
256
+ @post = Post.find(env[:id])
257
+ # calling `super` will populate the instance-level `env` hash, making
258
+ # `env` available outside this method. But it's fine to skip it.
259
+ super
235
260
  end
236
- end
237
261
 
238
- PostAnalysis.memoized(post: Post.find(1)) #=> slow, because it runs an expensive natural language analysis
239
- PostAnalysis.memoized(post: Post.find(1)) #=> instant, because the content of :post has not changed
262
+ render erb: <<~ERB
263
+ <article>
264
+ <h1><%= @post.title %></h1>
265
+ <%= @post.body %>
266
+ </article>
267
+ ERB
268
+ end
240
269
  ```
241
270
 
242
- This recipe works with any Ruby object that implements a `#hash` method based
243
- on the object's content, including instances of `ActiveRecord::Base` and
244
- `Sequel::Model`.
245
-
246
271
  ### Render an HTML list from an array
247
272
 
248
273
  [JSX Lists][jsx lists] use JavaScript's `map` function. Rack::Component does
@@ -251,7 +276,7 @@ likewise, only you need to call `join` on the array:
251
276
  ```ruby
252
277
  require 'rack/component'
253
278
  class PostsList < Rack::Component
254
- render do |env|
279
+ render do
255
280
  <<~HTML
256
281
  <h1>This is a list of posts</h1>
257
282
  <ul>
@@ -264,12 +289,12 @@ class PostsList < Rack::Component
264
289
  env[:posts].map { |post|
265
290
  <<~HTML
266
291
  <li class="item">
267
- <a href="#{post[:url]}">
292
+ <a href="/posts/#{post[:id]}">
268
293
  #{post[:name]}
269
294
  </a>
270
295
  </li>
271
296
  HTML
272
- }.join #unlike JSX, you need to call `join` on your array
297
+ }.join # unlike JSX, you need to call `join` on your array
273
298
  end
274
299
  end
275
300
 
@@ -289,26 +314,24 @@ end
289
314
 
290
315
  # app/components/posts_list.rb
291
316
  class PostsList < Rack::Component
292
- render { |env| posts.to_json }
293
-
294
- def posts
295
- Post.magically_filter_via_params(env)
317
+ def render
318
+ Post.magically_filter_via_params(env).to_json
296
319
  end
297
320
  end
298
321
  ```
299
322
 
300
323
  ### Mount a Rack::Component as a Rack app
301
324
 
302
- Because Rack::Components follow the same API as a Rack app, you can mount them
303
- anywhere you can mount a Rack app. It's up to you to return a valid rack
304
- tuple, though.
325
+ Because Rack::Components have the same signature as Rack app, you can mount them
326
+ anywhere you can mount a Rack app. It's up to you to return a valid rack tuple,
327
+ though.
305
328
 
306
329
  ```ruby
307
330
  # config.ru
308
331
  require 'rack/component'
309
332
 
310
333
  class Posts < Rack::Component
311
- render do |env|
334
+ def render
312
335
  [status, headers, [body]]
313
336
  end
314
337
 
@@ -335,66 +358,102 @@ Rack::Component instead of Controllers, Views, and templates. But to see an
335
358
  entire app built only out of Rack::Components, see
336
359
  [the example spec](https://github.com/chrisfrank/rack-component/blob/master/spec/raw_rack_example_spec.rb).
337
360
 
361
+ ### Define `#render` at the instance level instead of via `render do`
362
+
363
+ The class-level `render` macro exists to make using templates easy, and to lean
364
+ on Ruby's keyword arguments as a limited imitation of React's `defaultProps` and
365
+ `PropTypes`. But you can define render at the instance level instead.
366
+
367
+ ```ruby
368
+ # these two components render identical output
369
+
370
+ class MacroComponent < Rack::Component
371
+ render do |name:, dept: 'Engineering'|
372
+ "#{name} - #{dept}"
373
+ end
374
+ end
375
+
376
+ class ExplicitComponent < Rack::Component
377
+ def initialize(name:, dept: 'Engineering')
378
+ @name = name
379
+ @dept = dept
380
+ # calling `super` will populate the instance-level `env` hash, making
381
+ # `env` available outside this method. But it's fine to skip it.
382
+ super
383
+ end
384
+
385
+ def render
386
+ "#{@name} - #{@dept}"
387
+ end
388
+ end
389
+ ```
390
+
338
391
  ## API Reference
339
392
 
340
393
  The full API reference is available here:
341
394
 
342
395
  https://www.rubydoc.info/gems/rack-component
343
396
 
344
- For info on how to clear or change the size of the memoziation cache, please see
345
- [the spec][spec].
346
-
347
397
  ## Performance
348
398
 
349
- On my machine, Rendering a Rack::Component is almost 10x faster than rendering a
350
- comparable Tilt template, and almost 100x faster than ERB from the Ruby standard
351
- library. Run `ruby spec/benchmarks.rb` to see what to expect in your env.
399
+ Run `ruby spec/benchmarks.rb` to see what to expect in your environment. These
400
+ results are from a 2015 iMac:
352
401
 
353
402
  ```
354
403
  $ ruby spec/benchmarks.rb
355
404
  Warming up --------------------------------------
356
- Ruby stdlib ERB 2.807k i/100ms
357
- Tilt (cached) 28.611k i/100ms
358
- Lambda 249.958k i/100ms
359
- Component 161.176k i/100ms
360
- Component [memoized] 94.586k i/100ms
405
+ stdlib ERB 2.682k i/100ms
406
+ Tilt ERB 15.958k i/100ms
407
+ Bare lambda 77.124k i/100ms
408
+ RC [def render] 64.905k i/100ms
409
+ RC [render do] 57.725k i/100ms
410
+ RC [render erb:] 15.595k i/100ms
361
411
  Calculating -------------------------------------
362
- Ruby stdlib ERB 29.296k2.0%) i/s - 148.771k in 5.080274s
363
- Tilt (cached) 319.935k (± 2.8%) i/s - 1.602M in 5.012009s
364
- Lambda 6.261M1.2%) i/s - 31.495M in 5.031302s
365
- Component 2.773M (± 1.8%) i/s - 14.022M in 5.057528s
366
- Component [memoized] 1.276M0.9%) i/s - 6.432M in 5.041348s
412
+ stdlib ERB 27.423k1.8%) i/s - 139.464k in 5.087391s
413
+ Tilt ERB 169.351k (± 2.2%) i/s - 861.732k in 5.090920s
414
+ Bare lambda 929.473k3.0%) i/s - 4.705M in 5.065991s
415
+ RC [def render] 775.176k (± 1.1%) i/s - 3.894M in 5.024347s
416
+ RC [render do] 686.653k2.3%) i/s - 3.464M in 5.046728s
417
+ RC [render erb:] 165.113k (± 1.7%) i/s - 826.535k in 5.007444s
367
418
  ```
368
419
 
369
- Notice that using `Component#memoized` is _slower_ than using `Component#call`
370
- in this benchmark. Because these components do almost nothing, it's more work to
371
- check the memoziation cache than to just render. For components that don't
372
- access a database, don't do network I/O, and aren't very CPU-intensive, it's
373
- probably fastest not to memoize. For components that do I/O, using `#memoize`
374
- can speed things up by several orders of magnitude.
420
+ Every component in the benchmark is configured to escape HTML when rendering.
421
+ When rendering via a block, Rack::Component is about 25x faster than ERB and 4x
422
+ faster than Tilt. When rendering a template via Tilt, it (unsurprisingly)
423
+ performs roughly at tilt-speed.
375
424
 
376
425
  ## Compatibility
377
426
 
378
- Rack::Component has zero dependencies, and will work in any Rack app. It should
379
- even work _outside_ a Rack app, because it's not actually dependent on Rack. I
380
- packaged it under the Rack namespace because it follows the Rack `call`
381
- specification, and because that's where I use and test it.
427
+ When not rendering Tilt templates, Rack::Component has zero dependencies,
428
+ and will work in any Rack app. It should even work _outside_ a Rack app, because
429
+ it's not actually dependent on Rack. I packaged it under the Rack namespace
430
+ because it follows the Rack `call` specification, and because that's where I
431
+ use and test it.
432
+
433
+ When using Tilt templates, you will need `tilt` and a templating gem in your
434
+ `Gemfile`:
435
+
436
+ ```ruby
437
+ gem 'tilt'
438
+ gem 'erubi' # or gem 'haml', etc
439
+ gem 'rack-component'
440
+ ```
382
441
 
383
442
  ## Anybody using this in production?
384
443
 
385
444
  Aye:
386
445
 
387
- - [future.com](https://www.future.com/)
388
- - [Seattle & King County Homelessness Response System](https://hrs.kc.future.com/)
446
+ * [future.com](https://www.future.com/)
447
+ * [Seattle & King County Homelessness Response System](https://hrs.kc.future.com/)
389
448
 
390
449
  ## Ruby reference
391
450
 
392
451
  Where React uses [JSX] to make components more ergonomic, Rack::Component leans
393
452
  heavily on some features built into the Ruby language, specifically:
394
453
 
395
- - [Heredocs]
396
- - [String Interpolation]
397
- - [Calling methods with a block][ruby blocks]
454
+ * [Heredocs]
455
+ * [String Interpolation]
456
+ * [Calling methods with a block][ruby blocks]
398
457
 
399
458
  ## Development
400
459
 
@@ -426,3 +485,4 @@ MIT
426
485
  [ruby blocks]: https://mixandgo.com/learn/mastering-ruby-blocks-in-less-than-5-minutes
427
486
  [roda]: http://roda.jeremyevans.net
428
487
  [sinatra]: http://sinatrarb.com
488
+ [tilt]: https://github.com/rtomayko/tilt
@@ -1,131 +1,86 @@
1
1
  require_relative 'component/version'
2
- require_relative 'component/memory_cache'
2
+ require_relative 'component/renderer'
3
3
  require 'cgi'
4
4
 
5
5
  module Rack
6
6
  # Subclass Rack::Component to compose functional, declarative responses to
7
7
  # HTTP requests.
8
+ # @example Subclass Rack::Component to compose functional, declarative
9
+ # responses to HTTP requests.
10
+ # class Greeter < Rack::Component
11
+ # render { "Hi, #{env[:name]" }
12
+ # end
8
13
  class Component
9
- class << self
10
- # Instantiate a new component with given +env+ return its rendered output.
11
- # @example Render a child block inside an HTML document
12
- # class Layout < Rack::Component
13
- # render do |env, &child|
14
- # <<~HTML
15
- # <!DOCTYPE html>
16
- # <html>
17
- # <head>
18
- # <title>#{env[:title]}</title>
19
- # </head>
20
- # <body>#{child.call}</body>
21
- # </html>
22
- # HTML
23
- # end
24
- # end
25
- #
26
- # Layout.call(title: 'Hello') { "<h1>Hello from Rack::Component" } #=>
27
- # # <!DOCTYPE html>
28
- # # <html>
29
- # # <head>
30
- # # <title>Hello</title>
31
- # # </head>
32
- # # <body><h1>Hello from Rack::Component</h1></body>
33
- # # </html>
34
- def call(env = {}, &child)
35
- new(env).call env, &child
14
+ # @example If you don't want to subclass, you can extend
15
+ # Rack::Component::Methods instead.
16
+ # class POROGreeter
17
+ # extend Rack::Component::Methods
18
+ # render { "Hi, #{env[:name]" }
19
+ # end
20
+ module Methods
21
+ def self.extended(base)
22
+ base.include(InstanceMethods)
36
23
  end
37
24
 
38
- # Use +memoized+ instead of +call+ to memoize the result of +call(env)+
39
- # and return it. Subsequent uses of +memoized(env)+ with the same +env+
40
- # will be read from a threadsafe in-memory cache, not computed.
41
- # @example Cache a slow network call
42
- # class Fetcher < Rack::Component
43
- # render do |env|
44
- # Net::HTTP.get(env[:uri]).to_json
45
- # end
46
- # end
47
- #
48
- # Fetcher.memoized(uri: '/slow/api.json')
49
- # # ...
50
- # # many seconds later...
51
- # # => { some: "data" }
52
- #
53
- # Fetcher.memoized(uri: '/slow/api.json') #=> instant! { some: "data" }
54
- # Fetcher.memoized(uri: '/other/source.json') #=> slow again!
55
- def memoized(env = {}, &child)
56
- cache.fetch(env.hash) { call(env, &child) }
25
+ def render(opts = {})
26
+ block_given? ? configure_block(Proc.new) : configure_template(opts)
57
27
  end
58
28
 
59
- # Forget all memoized calls to this component.
60
- def flush
61
- cache.flush
29
+ def call(env = {}, &children)
30
+ new(env).render(&children)
62
31
  end
63
32
 
64
- # Use a +render+ block define what a component will do when you +call+ it.
65
- # @example Say hello
66
- # class Greeter < Rack::Component
67
- # render do |env|
68
- # "Hi, #{env[:name]}"
69
- # end
70
- # end
71
- #
72
- # Greeter.call(name: 'Jim') #=> 'Hi, Jim'
73
- # Greeter.call(name: 'Bones') #=> 'Hi, Bones'
74
- def render(&block)
75
- define_method :call, &block
76
- end
33
+ # Instances of Rack::Component come with these methods.
34
+ # :reek:ModuleInitialize
35
+ module InstanceMethods
36
+ # +env+ is Rack::Component's version of React's +props+ hash.
37
+ def initialize(env)
38
+ @env = env
39
+ end
40
+
41
+ # +env+ can be an empty hash, but cannot be nil
42
+ # @return [Hash]
43
+ def env
44
+ @env || {}
45
+ end
77
46
 
78
- # Find or initialize a cache store for a Component class.
79
- # With no configuration, the store is a threadsafe in-memory cache, capped
80
- # at 100 keys in length to avoid leaking RAM.
81
- # @example Use a larger cache instead
82
- # class BigComponent < Rack::Component
83
- # cache { MemoryCache.new(length: 2000) }
84
- # end
85
- def cache
86
- @cache ||= (block_given? ? yield : MemoryCache.new(length: 100))
47
+ # +h+ removes HTML characters from strings via +CGI.escapeHTML+.
48
+ # @return [String]
49
+ def h(obj)
50
+ CGI.escapeHTML(obj.to_s)
51
+ end
87
52
  end
88
- end
89
53
 
90
- def initialize(env = {})
91
- @env = env
92
- end
54
+ private
93
55
 
94
- # Out of the box, a +Rack::Component+ just returns whatever +env+ you call
95
- # it with, or yields with +env+ if you call it with a block.
96
- # Use a class-level +render+ block when wiriting your Components to override
97
- # this method with more useful behavior.
98
- # @see Rack::Component#render
99
- #
100
- # @example a useless component
101
- # Useless = Class.new(Rack::Component)
102
- # Useless.call(number: 1) #=> { number: 1 }
103
- # Useless.call(number: 2) #=> { number: 2 }
104
- # Useless.call(number: 2) { |env| "the number was #{env[:number]" }
105
- # #=> 'the number was 2'
106
- #
107
- # @example a useful component
108
- # class Greeter < Rack::Component
109
- # render do |env|
110
- # "Hi, #{env[:name]}"
111
- # end
112
- # end
113
- #
114
- # Greeter.call(name: 'Jim') #=> 'Hi, Jim'
115
- # Greeter.call(name: 'Bones') #=> 'Hi, Bones'
116
- def call(*)
117
- block_given? ? yield(env) : env
118
- end
56
+ # :reek:TooManyStatements
57
+ # :reek:DuplicateMethodCall
58
+ def configure_block(block)
59
+ # Convert the block to an instance method, because instance_exec
60
+ # doesn't allow passing an &child param, and because it's faster.
61
+ define_method :_rc_render, &block
62
+ private :_rc_render
119
63
 
120
- attr_reader :env
64
+ # Now that the block is a method, it must be called with the correct
65
+ # number of arguments. Ruby's +arity+ method is unreliable when keyword
66
+ # args are involved, so we count arity by hand.
67
+ arity = block.parameters.reject { |type, _| type == :block }.length
121
68
 
122
- # @example Strip HTML entities from a string
123
- # class SafeComponent < Rack::Component
124
- # render { |env| h(env[:name]) }
125
- # end
126
- # SafeComponent.call(name: '<h1>hi</h1>') #=> &lt;h1&gt;hi&lt;/h1&gt;
127
- def h(obj)
128
- CGI.escapeHTML(obj.to_s)
69
+ # Reek hates this DuplicateMethodCall, but fixing it would mean checking
70
+ # arity at runtime, rather than when the render macro is called.
71
+ if arity.zero?
72
+ define_method(:render) { |&child| _rc_render(&child) }
73
+ else
74
+ define_method(:render) { |&child| _rc_render(env, &child) }
75
+ end
76
+ end
77
+
78
+ def configure_template(options)
79
+ renderer = Renderer.new(options)
80
+ define_method(:render) { |&child| renderer.call(self, &child) }
81
+ end
129
82
  end
83
+
84
+ extend Methods
130
85
  end
131
86
  end
@@ -0,0 +1,31 @@
1
+ module Rack
2
+ class Component
3
+ # Compile a Tilt template, which a component will render
4
+ class Renderer
5
+ DEFAULT_TILT_OPTIONS = { escape_html: true }.freeze
6
+ FORMATS = %i[erb rhtml erubis haml liquid markdown md mkd].freeze
7
+
8
+ def initialize(options = {})
9
+ require 'tilt'
10
+ engine, template, @config = OptionParser.call(options)
11
+ require 'erubi' if engine == 'erb' && @config[:escape_html]
12
+ @template = Tilt[engine].new(@config) { template }
13
+ end
14
+
15
+ def call(scope, &child)
16
+ @template.render(scope, &child)
17
+ end
18
+
19
+ OptionParser = lambda do |opts|
20
+ tilt_options = DEFAULT_TILT_OPTIONS.merge(opts.delete(:opts) || {})
21
+ engine, template =
22
+ opts.find { |key, _| FORMATS.include?(key) } ||
23
+ [opts[:engine], opts[:template]]
24
+
25
+ [engine.to_s, template, tilt_options]
26
+ end
27
+
28
+ private_constant :OptionParser
29
+ end
30
+ end
31
+ end
@@ -1,5 +1,5 @@
1
1
  module Rack
2
2
  class Component
3
- VERSION = '0.4.2'.freeze
3
+ VERSION = '0.5.0'.freeze
4
4
  end
5
5
  end
@@ -30,10 +30,13 @@ Gem::Specification.new do |spec|
30
30
  spec.executables = spec.files.grep(%r{^exe/}) { |f| File.basename(f) }
31
31
  spec.require_paths = ['lib']
32
32
 
33
- spec.required_ruby_version = '>= 2.2'
33
+ spec.required_ruby_version = '>= 2.3'
34
34
 
35
35
  spec.add_development_dependency 'benchmark-ips', '~> 2.7'
36
- spec.add_development_dependency 'bundler', '~> 1.16'
36
+ spec.add_development_dependency 'bundler', '~> 2'
37
+ spec.add_development_dependency 'erubi', '~> 1.8'
38
+ spec.add_development_dependency 'haml', '~> 5'
39
+ spec.add_development_dependency 'liquid', '~> 4'
37
40
  spec.add_development_dependency 'pry', '~> 0.11'
38
41
  spec.add_development_dependency 'rack', '~> 2.0.6'
39
42
  spec.add_development_dependency 'rack-test', '~> 0'
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: rack-component
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.4.2
4
+ version: 0.5.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Chris Frank
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2019-01-04 00:00:00.000000000 Z
11
+ date: 2019-01-14 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: benchmark-ips
@@ -30,14 +30,56 @@ dependencies:
30
30
  requirements:
31
31
  - - "~>"
32
32
  - !ruby/object:Gem::Version
33
- version: '1.16'
33
+ version: '2'
34
+ type: :development
35
+ prerelease: false
36
+ version_requirements: !ruby/object:Gem::Requirement
37
+ requirements:
38
+ - - "~>"
39
+ - !ruby/object:Gem::Version
40
+ version: '2'
41
+ - !ruby/object:Gem::Dependency
42
+ name: erubi
43
+ requirement: !ruby/object:Gem::Requirement
44
+ requirements:
45
+ - - "~>"
46
+ - !ruby/object:Gem::Version
47
+ version: '1.8'
48
+ type: :development
49
+ prerelease: false
50
+ version_requirements: !ruby/object:Gem::Requirement
51
+ requirements:
52
+ - - "~>"
53
+ - !ruby/object:Gem::Version
54
+ version: '1.8'
55
+ - !ruby/object:Gem::Dependency
56
+ name: haml
57
+ requirement: !ruby/object:Gem::Requirement
58
+ requirements:
59
+ - - "~>"
60
+ - !ruby/object:Gem::Version
61
+ version: '5'
62
+ type: :development
63
+ prerelease: false
64
+ version_requirements: !ruby/object:Gem::Requirement
65
+ requirements:
66
+ - - "~>"
67
+ - !ruby/object:Gem::Version
68
+ version: '5'
69
+ - !ruby/object:Gem::Dependency
70
+ name: liquid
71
+ requirement: !ruby/object:Gem::Requirement
72
+ requirements:
73
+ - - "~>"
74
+ - !ruby/object:Gem::Version
75
+ version: '4'
34
76
  type: :development
35
77
  prerelease: false
36
78
  version_requirements: !ruby/object:Gem::Requirement
37
79
  requirements:
38
80
  - - "~>"
39
81
  - !ruby/object:Gem::Version
40
- version: '1.16'
82
+ version: '4'
41
83
  - !ruby/object:Gem::Dependency
42
84
  name: pry
43
85
  requirement: !ruby/object:Gem::Requirement
@@ -176,6 +218,7 @@ files:
176
218
  - ".rspec"
177
219
  - ".rubocop.yml"
178
220
  - ".travis.yml"
221
+ - CHANGELOG.md
179
222
  - Gemfile
180
223
  - Gemfile.lock
181
224
  - README.md
@@ -184,6 +227,7 @@ files:
184
227
  - bin/setup
185
228
  - lib/rack/component.rb
186
229
  - lib/rack/component/memory_cache.rb
230
+ - lib/rack/component/renderer.rb
187
231
  - lib/rack/component/version.rb
188
232
  - rack-component.gemspec
189
233
  homepage: https://www.github.com/chrisfrank/rack-component
@@ -199,7 +243,7 @@ required_ruby_version: !ruby/object:Gem::Requirement
199
243
  requirements:
200
244
  - - ">="
201
245
  - !ruby/object:Gem::Version
202
- version: '2.2'
246
+ version: '2.3'
203
247
  required_rubygems_version: !ruby/object:Gem::Requirement
204
248
  requirements:
205
249
  - - ">="