rack-component 0.4.2 → 0.5.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
  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
  - - ">="