rack-component 0.2.0 → 0.3.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: 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