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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 441fd93e02f16449c7284aa7c84b72d01bc8b87e133999c2ed4b7a637431be07
4
- data.tar.gz: ab7f5669c4b28c991f173180597f7b94c64de8cc8158d31da18f60b3927010f8
3
+ metadata.gz: 98202bd82c61277b38d1300b503c561372f617ef49ff5f61a7ec6582830d1d25
4
+ data.tar.gz: ddb88dacbe4bd2c60a3ba337b08ded9d6b9151e01300c2226ef428bd5ab83aa1
5
5
  SHA512:
6
- metadata.gz: 3232586822a9c38192cdbab6aa537236a331334ec0fe5ccaefbcdb8a62be92617f9fa94c4f372dbefc6eed92d0a8ef34e4c91971c6ef8b5f211bfede3d52b470
7
- data.tar.gz: dbe6b3663319d39bf6ebf5c5610979a5de5713ec992dfe6dc5f5f680bd7c551ed540036addf317fdd08c93756259a4deda97a49b224dbe8ec3b28fe593574c88
6
+ metadata.gz: 2185f7fad25bfaed06bcfc199d4986b71739c97ec3ce54d7336299c5eff49b6d5f8b050a6096ca7a761505c4c4d2a25bb5aeb04d9e320cc7df36fc8d974aad32
7
+ data.tar.gz: 892055b99db3a063ab79128fe4ef5e89de7d93055d8f006d5ad5197fde8c6aad48fff87db65078b93bc4a113e6aa9357683fda92dcae9a2776607859fc9998ee
data/.reek.yml CHANGED
@@ -3,6 +3,9 @@ detectors:
3
3
  enabled: false
4
4
  UncommunicativeVariableName:
5
5
  enabled: false
6
+ NestedIterators:
7
+ ignore_iterators:
8
+ - each_object
6
9
  exclude_paths:
7
10
  - bin
8
11
  - client
data/Gemfile.lock CHANGED
@@ -1,7 +1,7 @@
1
1
  PATH
2
2
  remote: .
3
3
  specs:
4
- rack-component (0.3.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.5)
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
- gem 'rack-component', require: 'rack/component'
7
+ bundle add 'rack-component'
15
8
  ```
16
9
 
17
- And then execute:
18
-
19
- $ bundle
20
-
21
- Or install it yourself as:
10
+ ## Get Started
22
11
 
23
- $ gem install rack-component
12
+ The simplest component is just a function:
24
13
 
25
- ## API Reference
26
-
27
- Please see the
28
- [YARD docs on rubydoc.info](https://www.rubydoc.info/gems/rack-component)
14
+ ```ruby
15
+ Greeter = lambda do |env|
16
+ "<h1>Hi, #{env[:name]}.</h1>"
17
+ end
29
18
 
30
- ## Usage
19
+ Greeter.call(name: 'Mina') #=> '<h1>Hi, Mina.</h1>'
20
+ ```
31
21
 
32
- Subclass `Rack::Component` and `#call` it:
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
- class Useless < Rack::Component
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
- Useless.call #=> the output Useless.new.render
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
- The default implementation of `#render` is to yield the component instance to
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
- Useless.call { |instance| "Hello from #{instance}" }
47
- #=> "Hello from #<Useless:0x00007fcaba87d138>"
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
- Useless.call do |instance|
50
- Useless.call do |second_instance|
51
- <<~HTML
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
- # <h1>Hello from #<Useless:0x00007fcaba87d138></h1>
59
- # <p>And also from #<Useless:0x00007f8482802498></p>
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
- ### Implement `#render` or add instance methods to make Components do work
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
- Peruse the [specs][specs] for examples of component chains that handle
65
- data fetching, views, and error handling in Sinatra and raw Rack.
79
+ ```ruby
80
+ Layout.call(title: 'Home') { Content.call }
81
+ ```
66
82
 
67
- Here's a component chain that prints headlines from Daring Fireball’s JSON feed:
83
+ Here's a more fully fleshed example:
68
84
 
69
85
  ```ruby
70
86
  require 'rack/component'
71
87
 
72
- # Make a network request and return the response
73
- class Fetcher < Rack::Component
74
- require 'net/http'
75
- def initialize(uri:)
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
- def render
80
- yield @response
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
- # Parse items from a JSON Feed document
85
- class JSONFeedParser < Rack::Component
86
- require 'json'
87
- def initialize(data)
88
- @items = JSON.parse(data).fetch('items')
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
- def render
92
- yield @items
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
- # Render an HTML list of posts
97
- class PostsList < Rack::Component
98
- def initialize(posts:, style: '')
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
- def render
137
+ ```ruby
138
+ require 'rack/component'
139
+ class PostsList < Rack::Component
140
+ render do |env|
104
141
  <<~HTML
105
- <ul style="#{@style}">
106
- #{@posts.map(&ListItem).join}"
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
- ListItem = ->(post) { "<li>#{post['title']}</li>" }
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
- # Fetch JSON Feed data from daring fireball, parse it, render a list
115
- Fetcher.call(uri: 'https://daringfireball.net/feeds/json') do |data|
116
- JSONFeedParser.call(data) do |items|
117
- PostsList.call(posts: items, style: 'background-color: red')
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
- end
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
- [specs]: https://github.com/chrisfrank/rack-component/tree/master/spec
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
- system 'bundle exec rubocop lib'
7
+ sh 'bundle exec rubocop lib'
8
8
  end
9
9
 
10
10
  task :reek do
11
- system 'bundle exec reek lib'
11
+ sh 'bundle exec reek lib'
12
12
  end
13
13
 
14
14
  task :doc do
15
- system 'bundle exec yard doc'
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
@@ -4,6 +4,5 @@ IFS=$'\n\t'
4
4
  set -vx
5
5
 
6
6
  bundle install
7
- bundle exec overcommit
8
7
 
9
8
  # Do any other automated setup that you need to do here
@@ -1,108 +1,121 @@
1
- require_relative 'component/component_cache'
2
- require_relative 'component/refinements'
1
+ require_relative 'component/version'
2
+ require_relative 'component/memory_cache'
3
3
 
4
4
  module Rack
5
- # Subclass Rack::Component to compose declarative, component-based responses
6
- # to HTTP requests
5
+ # Subclass Rack::Component to compose functional, declarative responses to
6
+ # HTTP requests.
7
7
  class Component
8
- VERSION = '0.3.0'.freeze
9
-
10
- # Initialize a new component with the given args and render it.
11
- #
12
- # @example Render a HelloWorld component
13
- # class HelloWorld < Rack::Component
14
- # def initialize(name)
15
- # @name = name
16
- # end
17
- #
18
- # def render
19
- # "<h1>Hello #{@name}</h1>"
20
- # end
21
- # end
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
- # def render
69
- # %(<h1>#{work}</h1>)
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
- # # first call takes five seconds
74
- # Expensive.call(id: 1) #=> <h1>1</h1>
75
- # # subsequent calls with identical args are instant
76
- # Expensive.call(id: 1) #=> <h1>1</h1>, instantly!
47
+ # Fetcher.memoized(uri: '/slow/api.json')
48
+ # # ...
49
+ # # many seconds later...
50
+ # # => { some: "data" }
77
51
  #
78
- # # subsequent calls with _different_ args take five seconds
79
- # Expensive.call(id: 2) #=> <h1>2</h1>
80
- #
81
- # @return [String, Object] the cached (or computed) output of render
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
- # Check the class-level cache, set it to &miss if nil.
87
- # @return [Object] the output of &miss.call
88
- def self.memoized(*args, &miss)
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
- # @return [Integer] a cache key for this component
93
- def self.key(*args)
94
- args.hash
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
- # Clear the cache of each descendant class.
98
- # Generally you'll call this on Rack::Component::Memoized directly.
99
- # @example Clear all caches:
100
- # Rack::Component::Memoized.clear_caches
101
- def self.clear_caches
102
- ObjectSpace.each_object(singleton_class) do |descendant|
103
- descendant.cache.flush
104
- end
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
@@ -0,0 +1,5 @@
1
+ module Rack
2
+ class Component
3
+ VERSION = '0.4.0'.freeze
4
+ end
5
+ end
@@ -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.3.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-21 00:00:00.000000000 Z
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/component_cache.rb
187
- - lib/rack/component/refinements.rb
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