rack-component 0.2.0 → 0.3.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: a4a43f4454b5d988c4745d26c61bb46b8cbe21e56e3aa74d6b84427d4f110ab7
4
- data.tar.gz: f49b907249aef3ebc79c5168c785eeb7ea977152c41278896e5cd4432467f2c5
3
+ metadata.gz: 441fd93e02f16449c7284aa7c84b72d01bc8b87e133999c2ed4b7a637431be07
4
+ data.tar.gz: ab7f5669c4b28c991f173180597f7b94c64de8cc8158d31da18f60b3927010f8
5
5
  SHA512:
6
- metadata.gz: a48bcaf0c62f6d83071d6a99ce5517898d28d498dc8f23ef61392641ea7a71a6b921f8f65521f35c4c79a001c90978594a9d439f4e9ab67dc0094840b43cfafb
7
- data.tar.gz: 87cd31cacc2c95b330e68cc07574f586c45777c69b0e4c5b6810fba0ff8325f4cb85b467fb718d35a201b3cbb7a09cf816c3ea7d2e05e5f0059d532c100e15be
6
+ metadata.gz: 3232586822a9c38192cdbab6aa537236a331334ec0fe5ccaefbcdb8a62be92617f9fa94c4f372dbefc6eed92d0a8ef34e4c91971c6ef8b5f211bfede3d52b470
7
+ data.tar.gz: dbe6b3663319d39bf6ebf5c5610979a5de5713ec992dfe6dc5f5f680bd7c551ed540036addf317fdd08c93756259a4deda97a49b224dbe8ec3b28fe593574c88
@@ -1,7 +1,7 @@
1
1
  PATH
2
2
  remote: .
3
3
  specs:
4
- rack-component (0.2.0)
4
+ rack-component (0.3.0)
5
5
 
6
6
  GEM
7
7
  remote: https://rubygems.org/
@@ -12,8 +12,6 @@ GEM
12
12
  ice_nine (~> 0.11.0)
13
13
  thread_safe (~> 0.3, >= 0.3.1)
14
14
  benchmark-ips (2.7.2)
15
- childprocess (0.9.0)
16
- ffi (~> 1.0, >= 1.0.11)
17
15
  codeclimate-engine-rb (0.4.1)
18
16
  virtus (~> 1.0)
19
17
  coderay (1.1.2)
@@ -23,16 +21,11 @@ GEM
23
21
  thread_safe (~> 0.3, >= 0.3.1)
24
22
  diff-lcs (1.3)
25
23
  equalizer (0.0.11)
26
- ffi (1.9.25)
27
24
  ice_nine (0.11.2)
28
- iniparse (1.4.4)
29
25
  jaro_winkler (1.5.1)
30
26
  kwalify (0.7.2)
31
27
  method_source (0.9.0)
32
28
  mustermann (1.0.3)
33
- overcommit (0.46.0)
34
- childprocess (~> 0.6, >= 0.6.3)
35
- iniparse (~> 1.4)
36
29
  parallel (1.12.1)
37
30
  parser (2.5.1.2)
38
31
  ast (~> 2.4.0)
@@ -95,7 +88,6 @@ PLATFORMS
95
88
  DEPENDENCIES
96
89
  benchmark-ips (~> 2.7)
97
90
  bundler (~> 1.16)
98
- overcommit (~> 0)
99
91
  pry (~> 0.11)
100
92
  rack-component!
101
93
  rack-test (~> 0)
data/README.md CHANGED
@@ -1,8 +1,10 @@
1
1
  # Rack::Component
2
2
 
3
- Like a React.js component, a `Rack::Component` implements a `render` method that takes input data and returns what to display.
3
+ Like a React.js component, a `Rack::Component` implements a `render` method that
4
+ takes input data and returns what to display.
4
5
 
5
- You can combine Components to build complex features out of simple, easily testable units.
6
+ You can combine Components to build complex features out of simple, easily
7
+ testable units.
6
8
 
7
9
  ## Installation
8
10
 
@@ -21,99 +23,103 @@ Or install it yourself as:
21
23
  $ gem install rack-component
22
24
 
23
25
  ## API Reference
24
- Please see the [YARD docs on rubydoc.info](https://www.rubydoc.info/gems/rack-component)
25
26
 
26
- ## Usage
27
+ Please see the
28
+ [YARD docs on rubydoc.info](https://www.rubydoc.info/gems/rack-component)
27
29
 
28
- You could build an entire app out of Components, but Ruby already has great HTTP routers like [Roda][roda] and [Sinatra][sinatra]. Here's an example that uses Sinatra for routing, and Components instead of views, controllers, and templates.
30
+ ## Usage
29
31
 
30
- ### With Sinatra
32
+ Subclass `Rack::Component` and `#call` it:
31
33
 
32
34
  ```ruby
33
- get '/posts/:id' do
34
- PostFetcher.call(id: params[:id]) do |post|
35
- Layout.call(title: post[:title]) do
36
- PostView.call(post)
37
- end
38
- end
35
+ require 'rack/component'
36
+ class Useless < Rack::Component
39
37
  end
38
+
39
+ Useless.call #=> the output Useless.new.render
40
40
  ```
41
41
 
42
- _Why_, you may be thinking, _would I write something so ugly when I could write this instead?_
42
+ The default implementation of `#render` is to yield the component instance to
43
+ whatever block you pass to `Component.call`, like this:
43
44
 
44
45
  ```ruby
45
- get '/posts/:id' do
46
- @post = Post.find(params[:id])
47
- @title = @post[:title]
48
- erb :post
46
+ Useless.call { |instance| "Hello from #{instance}" }
47
+ #=> "Hello from #<Useless:0x00007fcaba87d138>"
48
+
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
55
+ end
49
56
  end
57
+ # =>
58
+ # <h1>Hello from #<Useless:0x00007fcaba87d138></h1>
59
+ # <p>And also from #<Useless:0x00007f8482802498></p>
50
60
  ```
51
61
 
52
- You'd be right that the traditional version is shorter and pretter. But the Component version’s API is more declarative -- you are describing what you want, and leaving the details of _how to get it_ up to each Component, instead of writing implementation-specific details right in your route block.
62
+ ### Implement `#render` or add instance methods to make Components do work
53
63
 
54
- The Component version is easier to reuse, refactor, and test. And because Components are meant to be combined via composition, it's actually trivial to make a Component version that's even more concise:
64
+ Peruse the [specs][specs] for examples of component chains that handle
65
+ data fetching, views, and error handling in Sinatra and raw Rack.
66
+
67
+ Here's a component chain that prints headlines from Daring Fireball’s JSON feed:
55
68
 
56
69
  ```ruby
57
- get('/posts/:id') do
58
- PostPageView.call(id: params[:id])
59
- end
70
+ require 'rack/component'
71
+
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
60
78
 
61
- # Compose a few Components to save on typing
62
- class PostPageView < Rack::Component
63
79
  def render
64
- PostFetcher.call(id: props[:id]) do |post|
65
- Layout.call(title: post[:title]) { PostView.call(post) }
66
- end
80
+ yield @response
67
81
  end
68
82
  end
69
- ```
70
83
 
71
- PostFetcher, Layout, and PostView are all simple Rack::Components. Their implementation looks like this:
72
-
73
- ```ruby
74
- require 'rack/component'
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')
89
+ end
75
90
 
76
- # render an HTML page
77
- class Layout < Rack::Component
78
91
  def render
79
- %(
80
- <html>
81
- <head>
82
- <title>#{props[:title]}</title>
83
- </head>
84
- <body>
85
- #{yield}
86
- </body>
87
- </html>
88
- )
92
+ yield @items
89
93
  end
90
94
  end
91
95
 
92
- # Fetch a post, pass it to the next component
93
- class PostFetcher < Rack::Component
94
- def render
95
- yield fetch
96
+ # Render an HTML list of posts
97
+ class PostsList < Rack::Component
98
+ def initialize(posts:, style: '')
99
+ @posts = posts
100
+ @style = style
96
101
  end
97
102
 
98
- def fetch
99
- DB[:posts].fetch(props[:id].to_i)
103
+ def render
104
+ <<~HTML
105
+ <ul style="#{@style}">
106
+ #{@posts.map(&ListItem).join}"
107
+ </ul>
108
+ HTML
100
109
  end
101
- end
102
110
 
103
- # A fake database with a fake 'posts' table
104
- DB = { posts: { 1 => { title: 'Example Title', body: 'Example body' } } }
111
+ ListItem = ->(post) { "<li>#{post['title']}</li>" }
112
+ end
105
113
 
106
- # View a single post
107
- class PostView < Rack::Component
108
- def render
109
- %(
110
- <article>
111
- <h1>#{props[:title]}</h1>
112
- <p>#{props[:body]}</h1>
113
- </article>
114
- )
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')
115
118
  end
116
119
  end
120
+ end
121
+ #=> A <ul> full of headlines from Daring Fireball
122
+
117
123
  ```
118
124
 
119
125
  ## Development
@@ -137,5 +143,4 @@ https://github.com/chrisfrank/rack-component.
137
143
 
138
144
  MIT
139
145
 
140
- [roda]: https://github.com/jeremyevans/roda
141
- [sinatra]: https://github.com/sinatra/sinatra
146
+ [specs]: https://github.com/chrisfrank/rack-component/tree/master/spec
@@ -1,7 +1,7 @@
1
1
  #!/usr/bin/env ruby
2
2
 
3
3
  require 'bundler/setup'
4
- require 'chemical'
4
+ require 'rack/component'
5
5
 
6
6
  # You can add fixtures and/or initialization code here to make experimenting
7
7
  # with your gem easier. You can also use a different console, if you like.
@@ -1,50 +1,54 @@
1
1
  require_relative 'component/component_cache'
2
+ require_relative 'component/refinements'
2
3
 
3
4
  module Rack
4
5
  # Subclass Rack::Component to compose declarative, component-based responses
5
6
  # to HTTP requests
6
7
  class Component
7
- VERSION = '0.2.0'.freeze
8
+ VERSION = '0.3.0'.freeze
8
9
 
9
- EMPTY = ''.freeze # components render an empty body by default
10
- attr_reader :props
11
-
12
- # Initialize a new component with the given props and #render() it.
10
+ # Initialize a new component with the given args and render it.
13
11
  #
14
- # @example render a HelloWorld component
12
+ # @example Render a HelloWorld component
15
13
  # class HelloWorld < Rack::Component
16
- # def world
17
- # props[:world]
14
+ # def initialize(name)
15
+ # @name = name
18
16
  # end
19
17
  #
20
- # def call
21
- # %(<h1>Hello #{world}</h1>)
18
+ # def render
19
+ # "<h1>Hello #{@name}</h1>"
22
20
  # end
23
21
  # end
24
22
  #
25
23
  # MyComponent.call(world: 'Earth') #=> '<h1>Hello Earth</h1>'
26
24
  # @return [String, Object] the output of instance#render
27
- def self.call(*props, &block)
28
- new(*props).render(&block)
29
- end
30
-
31
- def initialize(props = {})
32
- @props = props
25
+ def self.call(*args, &block)
26
+ new(*args).render(&block)
33
27
  end
34
28
 
35
- # Override call to make your component do work.
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
+ #
36
34
  # @return [String, Object] usually a string, but really whatever
37
35
  def render
38
- block_given? ? yield(self) : EMPTY
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
39
43
  end
40
44
 
41
45
  # Rack::Component::Memoized is just like Component, only it
42
46
  # caches its rendered output in memory and only rerenders
43
- # when called with new props.
47
+ # when called with new arguments.
44
48
  class Memoized < self
45
- CACHE_SIZE = 100 # limit cache to 100 keys by default so we don't leak RAM
49
+ CACHE_SIZE = 100 # limit to 100 keys by default to prevent leaking RAM
46
50
 
47
- # instantiate a class-level cache if necessary
51
+ # Access or instantiate a class-level cache
48
52
  # @return [Rack::Component::ComponentCache] a threadsafe in-memory cache
49
53
  def self.cache
50
54
  @cache ||= ComponentCache.new(const_get(:CACHE_SIZE))
@@ -52,37 +56,42 @@ module Rack
52
56
 
53
57
  # @example render a Memoized Component
54
58
  # class Expensive < Rack::Component::Memoized
59
+ # def initialize(id)
60
+ # @id = id
61
+ # end
62
+ #
55
63
  # def work
56
64
  # sleep 5
57
- # "#{props[:id]} was expensive"
65
+ # "#{@id}"
58
66
  # end
59
67
  #
60
- # def call
68
+ # def render
61
69
  # %(<h1>#{work}</h1>)
62
70
  # end
63
71
  # end
64
72
  #
65
73
  # # first call takes five seconds
66
- # Expensive.call(id: 1) #=> <h1>1 was expensive</h1>
67
- # # subsequent calls with identical props are instant
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!
68
77
  #
69
- # # subsequent calls with _different_ props take five seconds
70
- # Expensive.call(id: 2) #=> <h1>2 was expensive</h1>
78
+ # # subsequent calls with _different_ args take five seconds
79
+ # Expensive.call(id: 2) #=> <h1>2</h1>
71
80
  #
72
81
  # @return [String, Object] the cached (or computed) output of render
73
- def self.call(*props, &block)
74
- memoized(*props) { super }
82
+ def self.call(*args, &block)
83
+ memoized(*args) { super }
75
84
  end
76
85
 
77
86
  # Check the class-level cache, set it to &miss if nil.
78
87
  # @return [Object] the output of &miss.call
79
- def self.memoized(*props, &miss)
80
- cache.fetch(key(*props), &miss)
88
+ def self.memoized(*args, &miss)
89
+ cache.fetch(key(*args), &miss)
81
90
  end
82
91
 
83
92
  # @return [Integer] a cache key for this component
84
- def self.key(*props)
85
- props.hash
93
+ def self.key(*args)
94
+ args.hash
86
95
  end
87
96
 
88
97
  # Clear the cache of each descendant class.
@@ -0,0 +1,14 @@
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
@@ -34,7 +34,6 @@ Gem::Specification.new do |spec|
34
34
 
35
35
  spec.add_development_dependency 'benchmark-ips', '~> 2.7'
36
36
  spec.add_development_dependency 'bundler', '~> 1.16'
37
- spec.add_development_dependency 'overcommit', '~> 0'
38
37
  spec.add_development_dependency 'pry', '~> 0.11'
39
38
  spec.add_development_dependency 'rack-test', '~> 0'
40
39
  spec.add_development_dependency 'rake', '~> 10.0'
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: rack-component
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.2.0
4
+ version: 0.3.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-19 00:00:00.000000000 Z
11
+ date: 2018-12-21 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: benchmark-ips
@@ -38,20 +38,6 @@ dependencies:
38
38
  - - "~>"
39
39
  - !ruby/object:Gem::Version
40
40
  version: '1.16'
41
- - !ruby/object:Gem::Dependency
42
- name: overcommit
43
- requirement: !ruby/object:Gem::Requirement
44
- requirements:
45
- - - "~>"
46
- - !ruby/object:Gem::Version
47
- version: '0'
48
- type: :development
49
- prerelease: false
50
- version_requirements: !ruby/object:Gem::Requirement
51
- requirements:
52
- - - "~>"
53
- - !ruby/object:Gem::Version
54
- version: '0'
55
41
  - !ruby/object:Gem::Dependency
56
42
  name: pry
57
43
  requirement: !ruby/object:Gem::Requirement
@@ -186,7 +172,6 @@ extensions: []
186
172
  extra_rdoc_files: []
187
173
  files:
188
174
  - ".gitignore"
189
- - ".overcommit.yml"
190
175
  - ".reek.yml"
191
176
  - ".rspec"
192
177
  - ".rubocop.yml"
@@ -199,6 +184,7 @@ files:
199
184
  - bin/setup
200
185
  - lib/rack/component.rb
201
186
  - lib/rack/component/component_cache.rb
187
+ - lib/rack/component/refinements.rb
202
188
  - rack-component.gemspec
203
189
  homepage: https://www.github.com/chrisfrank/rack-component
204
190
  licenses:
@@ -1,37 +0,0 @@
1
- # Use this file to configure the Overcommit hooks you wish to use. This will
2
- # extend the default configuration defined in:
3
- # https://github.com/brigade/overcommit/blob/master/config/default.yml
4
- #
5
- # At the topmost level of this YAML file is a key representing type of hook
6
- # being run (e.g. pre-commit, commit-msg, etc.). Within each type you can
7
- # customize each hook, such as whether to only run it on certain files (via
8
- # `include`), whether to only display output if it fails (via `quiet`), etc.
9
- #
10
- # For a complete list of hooks, see:
11
- # https://github.com/brigade/overcommit/tree/master/lib/overcommit/hook
12
- #
13
- # For a complete list of options that you can use to customize hooks, see:
14
- # https://github.com/brigade/overcommit#configuration
15
- #
16
- # Uncomment the following lines to make the configuration take effect.
17
-
18
- PreCommit:
19
- Rake:
20
- enabled: true
21
- command: ['bundle', 'exec', 'rake']
22
-
23
- # RuboCop:
24
- # enabled: true
25
- # on_warn: fail # Treat all warnings as failures
26
- #
27
- # TrailingWhitespace:
28
- # enabled: true
29
- # exclude:
30
- # - '**/db/structure.sql' # Ignore trailing whitespace in generated files
31
- #
32
- #PostCheckout:
33
- # ALL: # Special hook name that customizes all hooks of this type
34
- # quiet: true # Change all post-checkout hooks to only display output on failure
35
- #
36
- # IndexTags:
37
- # enabled: true # Generate a tags file with `ctags` each time HEAD changes