rack-component 0.3.0 → 0.4.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/.reek.yml +3 -0
- data/Gemfile.lock +3 -2
- data/README.md +203 -71
- data/Rakefile +8 -3
- data/bin/setup +0 -1
- data/lib/rack/component.rb +102 -89
- data/lib/rack/component/memory_cache.rb +41 -0
- data/lib/rack/component/version.rb +5 -0
- data/rack-component.gemspec +2 -2
- metadata +18 -18
- data/lib/rack/component/component_cache.rb +0 -45
- data/lib/rack/component/refinements.rb +0 -14
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 98202bd82c61277b38d1300b503c561372f617ef49ff5f61a7ec6582830d1d25
|
4
|
+
data.tar.gz: ddb88dacbe4bd2c60a3ba337b08ded9d6b9151e01300c2226ef428bd5ab83aa1
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 2185f7fad25bfaed06bcfc199d4986b71739c97ec3ce54d7336299c5eff49b6d5f8b050a6096ca7a761505c4c4d2a25bb5aeb04d9e320cc7df36fc8d974aad32
|
7
|
+
data.tar.gz: 892055b99db3a063ab79128fe4ef5e89de7d93055d8f006d5ad5197fde8c6aad48fff87db65078b93bc4a113e6aa9357683fda92dcae9a2776607859fc9998ee
|
data/.reek.yml
CHANGED
data/Gemfile.lock
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
PATH
|
2
2
|
remote: .
|
3
3
|
specs:
|
4
|
-
rack-component (0.
|
4
|
+
rack-component (0.4.0)
|
5
5
|
|
6
6
|
GEM
|
7
7
|
remote: https://rubygems.org/
|
@@ -33,7 +33,7 @@ GEM
|
|
33
33
|
pry (0.11.3)
|
34
34
|
coderay (~> 1.1.0)
|
35
35
|
method_source (~> 0.9.0)
|
36
|
-
rack (2.0.
|
36
|
+
rack (2.0.6)
|
37
37
|
rack-protection (2.0.4)
|
38
38
|
rack
|
39
39
|
rack-test (0.8.3)
|
@@ -89,6 +89,7 @@ DEPENDENCIES
|
|
89
89
|
benchmark-ips (~> 2.7)
|
90
90
|
bundler (~> 1.16)
|
91
91
|
pry (~> 0.11)
|
92
|
+
rack (~> 2.0.6)
|
92
93
|
rack-component!
|
93
94
|
rack-test (~> 0)
|
94
95
|
rake (~> 10.0)
|
data/README.md
CHANGED
@@ -3,125 +3,249 @@
|
|
3
3
|
Like a React.js component, a `Rack::Component` implements a `render` method that
|
4
4
|
takes input data and returns what to display.
|
5
5
|
|
6
|
-
You can combine Components to build complex features out of simple, easily
|
7
|
-
testable units.
|
8
|
-
|
9
|
-
## Installation
|
10
|
-
|
11
|
-
Add this line to your application's Gemfile:
|
12
|
-
|
13
6
|
```ruby
|
14
|
-
|
7
|
+
bundle add 'rack-component'
|
15
8
|
```
|
16
9
|
|
17
|
-
|
18
|
-
|
19
|
-
$ bundle
|
20
|
-
|
21
|
-
Or install it yourself as:
|
10
|
+
## Get Started
|
22
11
|
|
23
|
-
|
12
|
+
The simplest component is just a function:
|
24
13
|
|
25
|
-
|
26
|
-
|
27
|
-
|
28
|
-
|
14
|
+
```ruby
|
15
|
+
Greeter = lambda do |env|
|
16
|
+
"<h1>Hi, #{env[:name]}.</h1>"
|
17
|
+
end
|
29
18
|
|
30
|
-
|
19
|
+
Greeter.call(name: 'Mina') #=> '<h1>Hi, Mina.</h1>'
|
20
|
+
```
|
31
21
|
|
32
|
-
|
22
|
+
Convert your function to a `Rack::Component` when it needs instance methods or state:
|
33
23
|
|
34
24
|
```ruby
|
35
25
|
require 'rack/component'
|
36
|
-
|
26
|
+
|
27
|
+
class FormalGreeter < Rack::Component
|
28
|
+
render do |env|
|
29
|
+
"<h1>Hi, #{title} #{env[:name]}.</h1>"
|
30
|
+
end
|
31
|
+
|
32
|
+
def title
|
33
|
+
# the hash you pass to `call` is available as `env` in instance methods
|
34
|
+
env[:title] || "President"
|
35
|
+
end
|
37
36
|
end
|
38
37
|
|
39
|
-
|
38
|
+
FormalGreeter.call(name: 'Macron') #=> "<h1>Hi, President Macron.</h1>"
|
39
|
+
FormalGreeter.call(name: 'Merkel', title: 'Chancellor') #=> "<h1>Hi, Chancellor Merkel.</h1>"
|
40
40
|
```
|
41
41
|
|
42
|
-
|
43
|
-
whatever block you pass to `Component.call`, like this:
|
42
|
+
Replace `#call` with `#memoized` to make re-renders instant:
|
44
43
|
|
45
44
|
```ruby
|
46
|
-
|
47
|
-
|
45
|
+
require 'rack/component'
|
46
|
+
require 'net/http'
|
47
|
+
class NetworkGreeter < Rack::Component
|
48
|
+
render do |env|
|
49
|
+
"Hi, #{get_job_title_from_api} #{env[:name]}."
|
50
|
+
end
|
48
51
|
|
49
|
-
|
50
|
-
|
51
|
-
|
52
|
-
<h1>Hello from #{instance}</h1>
|
53
|
-
<p>And also from #{second_instance}"</p>
|
54
|
-
HTML
|
52
|
+
def get_job_title_from_api
|
53
|
+
endpoint = URI("http://api.heads-of-state.gov/")
|
54
|
+
Net::HTTP.get("#{endpoint}?q=#{env[:name]}")
|
55
55
|
end
|
56
56
|
end
|
57
|
-
|
58
|
-
|
59
|
-
#
|
57
|
+
|
58
|
+
NetworkGreeter.memoized(name: 'Macron')
|
59
|
+
# ...after a slow network call to our fictional Heads Of State API
|
60
|
+
#=> "Hi, President Macron."
|
61
|
+
|
62
|
+
NetworkGreeter.memoized(name: 'Macron') # subsequent calls with the same env are instant.
|
63
|
+
#=> "Hi, President Macron."
|
64
|
+
|
65
|
+
NetworkGreeter.memoized(name: 'Merkel')
|
66
|
+
# ...this env is new, so NetworkGreeter makes another network call
|
67
|
+
#=> "Hi, Chancellor Merkel."
|
68
|
+
|
69
|
+
NetworkGreeter.memoized(name: 'Merkel') #=> instant! "Hi, Chancellor Merkel."
|
70
|
+
NetworkGreeter.memoized(name: 'Macron') #=> instant! "Hi, President Macron."
|
60
71
|
```
|
61
72
|
|
62
|
-
|
73
|
+
## Recipes
|
74
|
+
|
75
|
+
### Render one component inside another
|
76
|
+
You can nest Rack::Components as if they were [React Children][JSX Children] by
|
77
|
+
calling them with a block.
|
63
78
|
|
64
|
-
|
65
|
-
|
79
|
+
```ruby
|
80
|
+
Layout.call(title: 'Home') { Content.call }
|
81
|
+
```
|
66
82
|
|
67
|
-
Here's a
|
83
|
+
Here's a more fully fleshed example:
|
68
84
|
|
69
85
|
```ruby
|
70
86
|
require 'rack/component'
|
71
87
|
|
72
|
-
#
|
73
|
-
|
74
|
-
|
75
|
-
|
76
|
-
@response = Net::HTTP.get(URI(uri))
|
77
|
-
end
|
88
|
+
# let's say this is a Sinatra app:
|
89
|
+
get '/posts/:id' do
|
90
|
+
PostPage.call(id: params[:id])
|
91
|
+
end
|
78
92
|
|
79
|
-
|
80
|
-
|
93
|
+
# fetch a post from the database and render it inside a layout
|
94
|
+
class PostPage < Rack::Component
|
95
|
+
render do |env|
|
96
|
+
post = Post.find(id: env[:id])
|
97
|
+
# Nest a PostContent instance inside a Layout instance
|
98
|
+
Layout.call(title: post.title) do
|
99
|
+
PostContent.call(title: post.title, body: post.body)
|
100
|
+
end
|
81
101
|
end
|
82
102
|
end
|
83
103
|
|
84
|
-
|
85
|
-
|
86
|
-
|
87
|
-
|
88
|
-
|
104
|
+
class PostContent < Rack::Component
|
105
|
+
render do |env|
|
106
|
+
<<~HTML
|
107
|
+
<article>
|
108
|
+
<h1>#{env[:title]}</h1>
|
109
|
+
#{env[:body]}
|
110
|
+
</article>
|
111
|
+
HTML
|
89
112
|
end
|
113
|
+
end
|
90
114
|
|
91
|
-
|
92
|
-
|
115
|
+
class Layout < Rack::Component
|
116
|
+
render do |env, &children|
|
117
|
+
# the `&children` param is just a standard ruby block
|
118
|
+
<<~HTML
|
119
|
+
<!DOCTYPE html>
|
120
|
+
<html>
|
121
|
+
<head>
|
122
|
+
<title>#{env[:title]}</title>
|
123
|
+
</head>
|
124
|
+
<body>
|
125
|
+
#{children.call}
|
126
|
+
</body>
|
127
|
+
</html>
|
128
|
+
HTML
|
93
129
|
end
|
94
130
|
end
|
131
|
+
```
|
95
132
|
|
96
|
-
|
97
|
-
|
98
|
-
|
99
|
-
@posts = posts
|
100
|
-
@style = style
|
101
|
-
end
|
133
|
+
### Render an HTML list from an array
|
134
|
+
[JSX Lists][JSX Lists] use JavaScript's `map` function. Rack::Component does
|
135
|
+
likewise.
|
102
136
|
|
103
|
-
|
137
|
+
```ruby
|
138
|
+
require 'rack/component'
|
139
|
+
class PostsList < Rack::Component
|
140
|
+
render do |env|
|
104
141
|
<<~HTML
|
105
|
-
<
|
106
|
-
|
142
|
+
<h1>This is a list of posts</h1>
|
143
|
+
<ul>
|
144
|
+
#{render_items}
|
107
145
|
</ul>
|
108
146
|
HTML
|
109
147
|
end
|
110
148
|
|
111
|
-
|
149
|
+
def render_items
|
150
|
+
env[:posts].map { |post|
|
151
|
+
<<~HTML
|
152
|
+
<li class="item">
|
153
|
+
<a href="#{post[:url]}>
|
154
|
+
#{post[:name]}
|
155
|
+
</a>
|
156
|
+
</li>
|
157
|
+
HTML
|
158
|
+
}.join #unlike, with JSX, you need to call `join` on your array
|
159
|
+
end
|
112
160
|
end
|
113
161
|
|
114
|
-
|
115
|
-
|
116
|
-
|
117
|
-
|
162
|
+
posts = [{ name: 'First Post', id: 1 }, { name: 'Second', id: 2 }]
|
163
|
+
PostsList.call(posts: posts) #=> <h1>This is a list of posts</h1> <ul>...etc
|
164
|
+
```
|
165
|
+
|
166
|
+
### Mount a Rack::Component tree inside a Rails app
|
167
|
+
For when just a few parts of your app are built with components:
|
168
|
+
|
169
|
+
```ruby
|
170
|
+
# config/routes.rb
|
171
|
+
mount MyComponent, at: '/a_path_of_your_choosing'
|
172
|
+
|
173
|
+
# config/initializers/my_component.rb
|
174
|
+
require 'rack/component'
|
175
|
+
class MyComponent < Rack::Component
|
176
|
+
render do |env|
|
177
|
+
<<~HTML
|
178
|
+
<h1>Hello from inside a Rails app!</h1>
|
179
|
+
HTML
|
118
180
|
end
|
119
181
|
end
|
120
|
-
|
121
|
-
#=> A <ul> full of headlines from Daring Fireball
|
182
|
+
```
|
122
183
|
|
184
|
+
### Build an entire Rack app out of Rack::Components
|
185
|
+
In real life, maybe don't do this. Use [Roda] or [Sinatra] for routing, and use
|
186
|
+
Rack::Component instead of Controllers, Views, and templates. But to see an
|
187
|
+
entire app built only out of Rack::Components, see
|
188
|
+
[the example spec](https://github.com/chrisfrank/rack-component/blob/master/spec/raw_rack_example_spec.rb).
|
189
|
+
|
190
|
+
|
191
|
+
## API Reference
|
192
|
+
The full API reference is available here:
|
193
|
+
|
194
|
+
https://www.rubydoc.info/gems/rack-component
|
195
|
+
|
196
|
+
For info on how to clear or change the size of the memoziation cache, please see
|
197
|
+
[the spec][spec].
|
198
|
+
|
199
|
+
## Performance
|
200
|
+
On my machine, Rendering a Rack::Component is almost 10x faster than rendering a
|
201
|
+
comparable Tilt template, and almost 100x faster than ERB from the Ruby standard
|
202
|
+
library. Run `ruby spec/benchmarks.rb` to see what to expect in your env.
|
203
|
+
|
204
|
+
```
|
205
|
+
$ ruby spec/benchmarks.rb
|
206
|
+
Warming up --------------------------------------
|
207
|
+
Ruby stdlib ERB 2.807k i/100ms
|
208
|
+
Tilt (cached) 28.611k i/100ms
|
209
|
+
Lambda 249.958k i/100ms
|
210
|
+
Component 161.176k i/100ms
|
211
|
+
Component [memoized] 94.586k i/100ms
|
212
|
+
Calculating -------------------------------------
|
213
|
+
Ruby stdlib ERB 29.296k (± 2.0%) i/s - 148.771k in 5.080274s
|
214
|
+
Tilt (cached) 319.935k (± 2.8%) i/s - 1.602M in 5.012009s
|
215
|
+
Lambda 6.261M (± 1.2%) i/s - 31.495M in 5.031302s
|
216
|
+
Component 2.773M (± 1.8%) i/s - 14.022M in 5.057528s
|
217
|
+
Component [memoized] 1.276M (± 0.9%) i/s - 6.432M in 5.041348s
|
123
218
|
```
|
124
219
|
|
220
|
+
Notice that using `Component#memoized` is *slower* than using `Component#call`
|
221
|
+
in this benchmark. Because these components do almost nothing, it's more work to
|
222
|
+
check the memoziation cache than to just render. For components that don't
|
223
|
+
access a database, don't do network I/O, and aren't very CPU-intensive, it's
|
224
|
+
probably fastest not to memoize. For components that do I/O, using `#memoize`
|
225
|
+
can speed things up by several orders of magnitude.
|
226
|
+
|
227
|
+
## Compatibility
|
228
|
+
Rack::Component has zero dependencies, and will work in any Rack app. It should
|
229
|
+
even work *outside* a Rack app, because it's not actually dependent on Rack. I
|
230
|
+
packaged it under the Rack namespace because it follows the Rack `call`
|
231
|
+
specification, and because that's where I use and test it.
|
232
|
+
|
233
|
+
## Anybody using this in production?
|
234
|
+
|
235
|
+
Aye:
|
236
|
+
|
237
|
+
- [future.com](https://www.future.com/)
|
238
|
+
- [Seattle & King County Homelessness Response System](https://hrs.kc.future.com/)
|
239
|
+
|
240
|
+
## Ruby reference:
|
241
|
+
|
242
|
+
Where React uses [JSX] to make components more ergonomic, Rack::Component leans
|
243
|
+
heavily on some features built into the Ruby language, specifically:
|
244
|
+
|
245
|
+
- [Heredocs]
|
246
|
+
- [String Interpolation]
|
247
|
+
- [Calling methods with a block][Ruby Blocks]
|
248
|
+
|
125
249
|
## Development
|
126
250
|
|
127
251
|
After checking out the repo, run `bin/setup` to install dependencies. Then, run
|
@@ -143,4 +267,12 @@ https://github.com/chrisfrank/rack-component.
|
|
143
267
|
|
144
268
|
MIT
|
145
269
|
|
146
|
-
[
|
270
|
+
[spec]: https://github.com/chrisfrank/rack-component/blob/master/spec/rack/component_spec.rb
|
271
|
+
[JSX]: https://reactjs.org/docs/introducing-jsx.html
|
272
|
+
[JSX Children]: https://reactjs.org/docs/composition-vs-inheritance.html
|
273
|
+
[JSX Lists]: https://reactjs.org/docs/lists-and-keys.html
|
274
|
+
[Heredocs]: https://ruby-doc.org/core-2.5.0/doc/syntax/literals_rdoc.html#label-Here+Documents
|
275
|
+
[String Interpolation]: http://ruby-for-beginners.rubymonstas.org/bonus/string_interpolation.html
|
276
|
+
[Ruby Blocks]: https://mixandgo.com/learn/mastering-ruby-blocks-in-less-than-5-minutes
|
277
|
+
[Roda]: http://roda.jeremyevans.net
|
278
|
+
[Sinatra]: http://sinatrarb.com
|
data/Rakefile
CHANGED
@@ -4,15 +4,20 @@ require 'rspec/core/rake_task'
|
|
4
4
|
RSpec::Core::RakeTask.new(:spec)
|
5
5
|
|
6
6
|
task :cop do
|
7
|
-
|
7
|
+
sh 'bundle exec rubocop lib'
|
8
8
|
end
|
9
9
|
|
10
10
|
task :reek do
|
11
|
-
|
11
|
+
sh 'bundle exec reek lib'
|
12
12
|
end
|
13
13
|
|
14
14
|
task :doc do
|
15
|
-
|
15
|
+
sh 'bundle exec yard doc'
|
16
|
+
end
|
17
|
+
|
18
|
+
task :commit do
|
19
|
+
sh 'bundle exec rake'
|
20
|
+
sh 'git add -A && git commit --verbose'
|
16
21
|
end
|
17
22
|
|
18
23
|
task default: %i[cop reek spec doc]
|
data/bin/setup
CHANGED
data/lib/rack/component.rb
CHANGED
@@ -1,108 +1,121 @@
|
|
1
|
-
require_relative 'component/
|
2
|
-
require_relative 'component/
|
1
|
+
require_relative 'component/version'
|
2
|
+
require_relative 'component/memory_cache'
|
3
3
|
|
4
4
|
module Rack
|
5
|
-
# Subclass Rack::Component to compose
|
6
|
-
#
|
5
|
+
# Subclass Rack::Component to compose functional, declarative responses to
|
6
|
+
# HTTP requests.
|
7
7
|
class Component
|
8
|
-
|
9
|
-
|
10
|
-
|
11
|
-
|
12
|
-
|
13
|
-
|
14
|
-
|
15
|
-
|
16
|
-
|
17
|
-
|
18
|
-
|
19
|
-
|
20
|
-
|
21
|
-
|
22
|
-
#
|
23
|
-
# MyComponent.call(world: 'Earth') #=> '<h1>Hello Earth</h1>'
|
24
|
-
# @return [String, Object] the output of instance#render
|
25
|
-
def self.call(*args, &block)
|
26
|
-
new(*args).render(&block)
|
27
|
-
end
|
28
|
-
|
29
|
-
# Override either #render or #exposures to make your component do work.
|
30
|
-
# By default, the behavior of #render depends on whether you call the
|
31
|
-
# component with a block or not: it either returns #exposures or yields to
|
32
|
-
# the block with #exposures as arguments.
|
33
|
-
#
|
34
|
-
# @return [String, Object] usually a string, but really whatever
|
35
|
-
def render
|
36
|
-
block_given? ? yield(exposures) : exposures
|
37
|
-
end
|
38
|
-
|
39
|
-
# Override #exposures to keep the default yield-or-return behavior
|
40
|
-
# of #render, but change what gets yielded or returned
|
41
|
-
def exposures
|
42
|
-
self
|
43
|
-
end
|
44
|
-
|
45
|
-
# Rack::Component::Memoized is just like Component, only it
|
46
|
-
# caches its rendered output in memory and only rerenders
|
47
|
-
# when called with new arguments.
|
48
|
-
class Memoized < self
|
49
|
-
CACHE_SIZE = 100 # limit to 100 keys by default to prevent leaking RAM
|
50
|
-
|
51
|
-
# Access or instantiate a class-level cache
|
52
|
-
# @return [Rack::Component::ComponentCache] a threadsafe in-memory cache
|
53
|
-
def self.cache
|
54
|
-
@cache ||= ComponentCache.new(const_get(:CACHE_SIZE))
|
55
|
-
end
|
56
|
-
|
57
|
-
# @example render a Memoized Component
|
58
|
-
# class Expensive < Rack::Component::Memoized
|
59
|
-
# def initialize(id)
|
60
|
-
# @id = id
|
61
|
-
# end
|
62
|
-
#
|
63
|
-
# def work
|
64
|
-
# sleep 5
|
65
|
-
# "#{@id}"
|
8
|
+
class << self
|
9
|
+
# Instantiate a new component with given +env+ return its rendered output.
|
10
|
+
# @example Render a child block inside an HTML document
|
11
|
+
# class Layout < Rack::Component
|
12
|
+
# render do |env, &child|
|
13
|
+
# <<~HTML
|
14
|
+
# <!DOCTYPE html>
|
15
|
+
# <html>
|
16
|
+
# <head>
|
17
|
+
# <title>#{env[:title]}</title>
|
18
|
+
# </head>
|
19
|
+
# <body>#{child.call}</body>
|
20
|
+
# </html>
|
21
|
+
# HTML
|
66
22
|
# end
|
23
|
+
# end
|
67
24
|
#
|
68
|
-
#
|
69
|
-
#
|
25
|
+
# Layout.call(title: 'Hello') { "<h1>Hello from Rack::Component" } #=>
|
26
|
+
# # <!DOCTYPE html>
|
27
|
+
# # <html>
|
28
|
+
# # <head>
|
29
|
+
# # <title>Hello</title>
|
30
|
+
# # </head>
|
31
|
+
# # <body><h1>Hello from Rack::Component</h1></body>
|
32
|
+
# # </html>
|
33
|
+
def call(env = {}, &child)
|
34
|
+
new(env).call env, &child
|
35
|
+
end
|
36
|
+
|
37
|
+
# Use +memoized+ instead of +call+ to memoize the result of +call(env)+
|
38
|
+
# and return it. Subsequent uses of +memoized(env)+ with the same +env+
|
39
|
+
# will be read from a threadsafe in-memory cache, not computed.
|
40
|
+
# @example Cache a slow network call
|
41
|
+
# class Fetcher < Rack::Component
|
42
|
+
# render do |env|
|
43
|
+
# Net::HTTP.get(env[:uri]).to_json
|
70
44
|
# end
|
71
45
|
# end
|
72
46
|
#
|
73
|
-
#
|
74
|
-
#
|
75
|
-
# #
|
76
|
-
#
|
47
|
+
# Fetcher.memoized(uri: '/slow/api.json')
|
48
|
+
# # ...
|
49
|
+
# # many seconds later...
|
50
|
+
# # => { some: "data" }
|
77
51
|
#
|
78
|
-
#
|
79
|
-
#
|
80
|
-
|
81
|
-
|
82
|
-
def self.call(*args, &block)
|
83
|
-
memoized(*args) { super }
|
52
|
+
# Fetcher.memoized(uri: '/slow/api.json') #=> instant! { some: "data" }
|
53
|
+
# Fetcher.memoized(uri: '/other/source.json') #=> slow again!
|
54
|
+
def memoized(env = {}, &child)
|
55
|
+
cache.fetch(env.hash) { call(env, &child) }
|
84
56
|
end
|
85
57
|
|
86
|
-
#
|
87
|
-
|
88
|
-
|
89
|
-
cache.fetch(key(*args), &miss)
|
58
|
+
# Forget all memoized calls to this component.
|
59
|
+
def flush
|
60
|
+
cache.flush
|
90
61
|
end
|
91
62
|
|
92
|
-
#
|
93
|
-
|
94
|
-
|
63
|
+
# Use a +render+ block define what a component will do when you +call+ it.
|
64
|
+
# @example Say hello
|
65
|
+
# class Greeter < Rack::Component
|
66
|
+
# render do |env|
|
67
|
+
# "Hi, #{env[:name]}"
|
68
|
+
# end
|
69
|
+
# end
|
70
|
+
#
|
71
|
+
# Greeter.call(name: 'Jim') #=> 'Hi, Jim'
|
72
|
+
# Greeter.call(name: 'Bones') #=> 'Hi, Bones'
|
73
|
+
def render(&block)
|
74
|
+
define_method :call, &block
|
95
75
|
end
|
96
76
|
|
97
|
-
#
|
98
|
-
#
|
99
|
-
#
|
100
|
-
#
|
101
|
-
|
102
|
-
|
103
|
-
|
104
|
-
|
77
|
+
# Find or initialize a cache store for a Component class.
|
78
|
+
# With no configuration, the store is a threadsafe in-memory cache, capped
|
79
|
+
# at 100 keys in length to avoid leaking RAM.
|
80
|
+
# @example Use a larger cache instead
|
81
|
+
# class BigComponent < Rack::Component
|
82
|
+
# cache { MemoryCache.new(length: 2000) }
|
83
|
+
# end
|
84
|
+
def cache
|
85
|
+
@cache ||= (block_given? ? yield : MemoryCache.new(length: 100))
|
105
86
|
end
|
106
87
|
end
|
88
|
+
|
89
|
+
def initialize(env = {})
|
90
|
+
@env = env
|
91
|
+
end
|
92
|
+
|
93
|
+
# Out of the box, a +Rack::Component+ just returns whatever +env+ you call
|
94
|
+
# it with, or yields with +env+ if you call it with a block.
|
95
|
+
# Use a class-level +render+ block when wiriting your Components to override
|
96
|
+
# this method with more useful behavior.
|
97
|
+
# @see Rack::Component#render
|
98
|
+
#
|
99
|
+
# @example a useless component
|
100
|
+
# Useless = Class.new(Rack::Component)
|
101
|
+
# Useless.call(number: 1) #=> { number: 1 }
|
102
|
+
# Useless.call(number: 2) #=> { number: 2 }
|
103
|
+
# Useless.call(number: 2) { |env| "the number was #{env[:number]" }
|
104
|
+
# #=> 'the number was 2'
|
105
|
+
#
|
106
|
+
# @example a useful component
|
107
|
+
# class Greeter < Rack::Component
|
108
|
+
# render do |env|
|
109
|
+
# "Hi, #{env[:name]}"
|
110
|
+
# end
|
111
|
+
# end
|
112
|
+
#
|
113
|
+
# Greeter.call(name: 'Jim') #=> 'Hi, Jim'
|
114
|
+
# Greeter.call(name: 'Bones') #=> 'Hi, Bones'
|
115
|
+
def call(*)
|
116
|
+
block_given? ? yield(env) : env
|
117
|
+
end
|
118
|
+
|
119
|
+
attr_reader :env
|
107
120
|
end
|
108
121
|
end
|
@@ -0,0 +1,41 @@
|
|
1
|
+
module Rack
|
2
|
+
class Component
|
3
|
+
# A threadsafe, in-memory, per-component cache
|
4
|
+
class MemoryCache
|
5
|
+
attr_reader :store, :mutex
|
6
|
+
|
7
|
+
# Use a hash to store cached calls and a mutex to make it threadsafe
|
8
|
+
def initialize(length: 100)
|
9
|
+
@store = {}
|
10
|
+
@length = length
|
11
|
+
@mutex = Mutex.new
|
12
|
+
end
|
13
|
+
|
14
|
+
# Fetch a key from the cache, if it exists
|
15
|
+
# If the key doesn't exist and a block is passed, set the key
|
16
|
+
# @return the cached value
|
17
|
+
def fetch(key)
|
18
|
+
store.fetch(key) do
|
19
|
+
set(key, yield) if block_given?
|
20
|
+
end
|
21
|
+
end
|
22
|
+
|
23
|
+
# Empty the cache
|
24
|
+
# @return [Hash] the empty store
|
25
|
+
def flush
|
26
|
+
mutex.synchronize { @store = {} }
|
27
|
+
end
|
28
|
+
|
29
|
+
private
|
30
|
+
|
31
|
+
# Cache a value and return it
|
32
|
+
def set(key, value)
|
33
|
+
mutex.synchronize do
|
34
|
+
store[key] = value
|
35
|
+
store.delete(@store.keys.first) if store.length > @length
|
36
|
+
value
|
37
|
+
end
|
38
|
+
end
|
39
|
+
end
|
40
|
+
end
|
41
|
+
end
|
data/rack-component.gemspec
CHANGED
@@ -1,6 +1,6 @@
|
|
1
1
|
lib = File.expand_path('lib', __dir__)
|
2
2
|
$LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
|
3
|
-
require 'rack/component'
|
3
|
+
require 'rack/component/version'
|
4
4
|
|
5
5
|
Gem::Specification.new do |spec|
|
6
6
|
spec.name = 'rack-component'
|
@@ -35,12 +35,12 @@ Gem::Specification.new do |spec|
|
|
35
35
|
spec.add_development_dependency 'benchmark-ips', '~> 2.7'
|
36
36
|
spec.add_development_dependency 'bundler', '~> 1.16'
|
37
37
|
spec.add_development_dependency 'pry', '~> 0.11'
|
38
|
+
spec.add_development_dependency 'rack', '~> 2.0.6'
|
38
39
|
spec.add_development_dependency 'rack-test', '~> 0'
|
39
40
|
spec.add_development_dependency 'rake', '~> 10.0'
|
40
41
|
spec.add_development_dependency 'reek', '~> 5'
|
41
42
|
spec.add_development_dependency 'rspec', '~> 3.0'
|
42
43
|
spec.add_development_dependency 'rubocop', '~> 0.59'
|
43
|
-
spec.add_development_dependency 'sinatra', '~> 2'
|
44
44
|
spec.add_development_dependency 'tilt', '~> 2'
|
45
45
|
spec.add_development_dependency 'yard', '~> 0.9'
|
46
46
|
end
|
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
|
+
version: 0.4.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: 2018-12-
|
11
|
+
date: 2018-12-28 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
14
|
name: benchmark-ips
|
@@ -52,6 +52,20 @@ dependencies:
|
|
52
52
|
- - "~>"
|
53
53
|
- !ruby/object:Gem::Version
|
54
54
|
version: '0.11'
|
55
|
+
- !ruby/object:Gem::Dependency
|
56
|
+
name: rack
|
57
|
+
requirement: !ruby/object:Gem::Requirement
|
58
|
+
requirements:
|
59
|
+
- - "~>"
|
60
|
+
- !ruby/object:Gem::Version
|
61
|
+
version: 2.0.6
|
62
|
+
type: :development
|
63
|
+
prerelease: false
|
64
|
+
version_requirements: !ruby/object:Gem::Requirement
|
65
|
+
requirements:
|
66
|
+
- - "~>"
|
67
|
+
- !ruby/object:Gem::Version
|
68
|
+
version: 2.0.6
|
55
69
|
- !ruby/object:Gem::Dependency
|
56
70
|
name: rack-test
|
57
71
|
requirement: !ruby/object:Gem::Requirement
|
@@ -122,20 +136,6 @@ dependencies:
|
|
122
136
|
- - "~>"
|
123
137
|
- !ruby/object:Gem::Version
|
124
138
|
version: '0.59'
|
125
|
-
- !ruby/object:Gem::Dependency
|
126
|
-
name: sinatra
|
127
|
-
requirement: !ruby/object:Gem::Requirement
|
128
|
-
requirements:
|
129
|
-
- - "~>"
|
130
|
-
- !ruby/object:Gem::Version
|
131
|
-
version: '2'
|
132
|
-
type: :development
|
133
|
-
prerelease: false
|
134
|
-
version_requirements: !ruby/object:Gem::Requirement
|
135
|
-
requirements:
|
136
|
-
- - "~>"
|
137
|
-
- !ruby/object:Gem::Version
|
138
|
-
version: '2'
|
139
139
|
- !ruby/object:Gem::Dependency
|
140
140
|
name: tilt
|
141
141
|
requirement: !ruby/object:Gem::Requirement
|
@@ -183,8 +183,8 @@ files:
|
|
183
183
|
- bin/console
|
184
184
|
- bin/setup
|
185
185
|
- lib/rack/component.rb
|
186
|
-
- lib/rack/component/
|
187
|
-
- lib/rack/component/
|
186
|
+
- lib/rack/component/memory_cache.rb
|
187
|
+
- lib/rack/component/version.rb
|
188
188
|
- rack-component.gemspec
|
189
189
|
homepage: https://www.github.com/chrisfrank/rack-component
|
190
190
|
licenses:
|
@@ -1,45 +0,0 @@
|
|
1
|
-
module Rack
|
2
|
-
class Component
|
3
|
-
# Threadsafe in-memory cache
|
4
|
-
class ComponentCache
|
5
|
-
attr_reader :store
|
6
|
-
|
7
|
-
# Initialize a mutex for threadsafe reads and writes
|
8
|
-
LOCK = Mutex.new
|
9
|
-
|
10
|
-
# Store cache in a hash
|
11
|
-
def initialize(limit = 100)
|
12
|
-
@store = {}
|
13
|
-
@limit = limit
|
14
|
-
end
|
15
|
-
|
16
|
-
# Fetch a key from the cache, if it exists
|
17
|
-
# If the key doesn't exist and a block is passed, set the key
|
18
|
-
# @return the cached value
|
19
|
-
def fetch(key)
|
20
|
-
store.fetch(key) do
|
21
|
-
write(key, yield) if block_given?
|
22
|
-
end
|
23
|
-
end
|
24
|
-
|
25
|
-
# empty the cache
|
26
|
-
# @return [Hash] the empty store
|
27
|
-
def flush
|
28
|
-
LOCK.synchronize { @store = {} }
|
29
|
-
end
|
30
|
-
|
31
|
-
private
|
32
|
-
|
33
|
-
# Cache a value and return it
|
34
|
-
def write(key, value)
|
35
|
-
LOCK.synchronize do
|
36
|
-
store[key] = value
|
37
|
-
store.delete(@store.keys.first) if store.length > @limit
|
38
|
-
value
|
39
|
-
end
|
40
|
-
end
|
41
|
-
end
|
42
|
-
|
43
|
-
private_constant :ComponentCache
|
44
|
-
end
|
45
|
-
end
|
@@ -1,14 +0,0 @@
|
|
1
|
-
module Rack
|
2
|
-
class Component
|
3
|
-
# These are a few refinements to the core classes to make rendering easier
|
4
|
-
module Refinements
|
5
|
-
refine Array do
|
6
|
-
# Join arrays with line breaks, so that calling list.map(&:render)
|
7
|
-
# results in usable HTML
|
8
|
-
def to_s
|
9
|
-
join("\n")
|
10
|
-
end
|
11
|
-
end
|
12
|
-
end
|
13
|
-
end
|
14
|
-
end
|