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 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