rack-component 0.3.0 → 0.4.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 +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
|