lib/rack/component.rb in rack-component-0.4.2 vs lib/rack/component.rb in rack-component-0.5.0
- old
+ new
@@ -1,131 +1,86 @@
require_relative 'component/version'
-require_relative 'component/memory_cache'
+require_relative 'component/renderer'
require 'cgi'
module Rack
# Subclass Rack::Component to compose functional, declarative responses to
# HTTP requests.
+ # @example Subclass Rack::Component to compose functional, declarative
+ # responses to HTTP requests.
+ # class Greeter < Rack::Component
+ # render { "Hi, #{env[:name]" }
+ # end
class Component
- class << self
- # Instantiate a new component with given +env+ return its rendered output.
- # @example Render a child block inside an HTML document
- # class Layout < Rack::Component
- # render do |env, &child|
- # <<~HTML
- # <!DOCTYPE html>
- # <html>
- # <head>
- # <title>#{env[:title]}</title>
- # </head>
- # <body>#{child.call}</body>
- # </html>
- # HTML
- # end
- # end
- #
- # Layout.call(title: 'Hello') { "<h1>Hello from Rack::Component" } #=>
- # # <!DOCTYPE html>
- # # <html>
- # # <head>
- # # <title>Hello</title>
- # # </head>
- # # <body><h1>Hello from Rack::Component</h1></body>
- # # </html>
- def call(env = {}, &child)
- new(env).call env, &child
+ # @example If you don't want to subclass, you can extend
+ # Rack::Component::Methods instead.
+ # class POROGreeter
+ # extend Rack::Component::Methods
+ # render { "Hi, #{env[:name]" }
+ # end
+ module Methods
+ def self.extended(base)
+ base.include(InstanceMethods)
end
- # Use +memoized+ instead of +call+ to memoize the result of +call(env)+
- # and return it. Subsequent uses of +memoized(env)+ with the same +env+
- # will be read from a threadsafe in-memory cache, not computed.
- # @example Cache a slow network call
- # class Fetcher < Rack::Component
- # render do |env|
- # Net::HTTP.get(env[:uri]).to_json
- # end
- # end
- #
- # Fetcher.memoized(uri: '/slow/api.json')
- # # ...
- # # many seconds later...
- # # => { some: "data" }
- #
- # Fetcher.memoized(uri: '/slow/api.json') #=> instant! { some: "data" }
- # Fetcher.memoized(uri: '/other/source.json') #=> slow again!
- def memoized(env = {}, &child)
- cache.fetch(env.hash) { call(env, &child) }
+ def render(opts = {})
+ block_given? ? configure_block(Proc.new) : configure_template(opts)
end
- # Forget all memoized calls to this component.
- def flush
- cache.flush
+ def call(env = {}, &children)
+ new(env).render(&children)
end
- # Use a +render+ block define what a component will do when you +call+ it.
- # @example Say hello
- # class Greeter < Rack::Component
- # render do |env|
- # "Hi, #{env[:name]}"
- # end
- # end
- #
- # Greeter.call(name: 'Jim') #=> 'Hi, Jim'
- # Greeter.call(name: 'Bones') #=> 'Hi, Bones'
- def render(&block)
- define_method :call, &block
- end
+ # Instances of Rack::Component come with these methods.
+ # :reek:ModuleInitialize
+ module InstanceMethods
+ # +env+ is Rack::Component's version of React's +props+ hash.
+ def initialize(env)
+ @env = env
+ end
- # Find or initialize a cache store for a Component class.
- # With no configuration, the store is a threadsafe in-memory cache, capped
- # at 100 keys in length to avoid leaking RAM.
- # @example Use a larger cache instead
- # class BigComponent < Rack::Component
- # cache { MemoryCache.new(length: 2000) }
- # end
- def cache
- @cache ||= (block_given? ? yield : MemoryCache.new(length: 100))
+ # +env+ can be an empty hash, but cannot be nil
+ # @return [Hash]
+ def env
+ @env || {}
+ end
+
+ # +h+ removes HTML characters from strings via +CGI.escapeHTML+.
+ # @return [String]
+ def h(obj)
+ CGI.escapeHTML(obj.to_s)
+ end
end
- end
- def initialize(env = {})
- @env = env
- end
+ private
- # Out of the box, a +Rack::Component+ just returns whatever +env+ you call
- # it with, or yields with +env+ if you call it with a block.
- # Use a class-level +render+ block when wiriting your Components to override
- # this method with more useful behavior.
- # @see Rack::Component#render
- #
- # @example a useless component
- # Useless = Class.new(Rack::Component)
- # Useless.call(number: 1) #=> { number: 1 }
- # Useless.call(number: 2) #=> { number: 2 }
- # Useless.call(number: 2) { |env| "the number was #{env[:number]" }
- # #=> 'the number was 2'
- #
- # @example a useful component
- # class Greeter < Rack::Component
- # render do |env|
- # "Hi, #{env[:name]}"
- # end
- # end
- #
- # Greeter.call(name: 'Jim') #=> 'Hi, Jim'
- # Greeter.call(name: 'Bones') #=> 'Hi, Bones'
- def call(*)
- block_given? ? yield(env) : env
- end
+ # :reek:TooManyStatements
+ # :reek:DuplicateMethodCall
+ def configure_block(block)
+ # Convert the block to an instance method, because instance_exec
+ # doesn't allow passing an &child param, and because it's faster.
+ define_method :_rc_render, &block
+ private :_rc_render
- attr_reader :env
+ # Now that the block is a method, it must be called with the correct
+ # number of arguments. Ruby's +arity+ method is unreliable when keyword
+ # args are involved, so we count arity by hand.
+ arity = block.parameters.reject { |type, _| type == :block }.length
- # @example Strip HTML entities from a string
- # class SafeComponent < Rack::Component
- # render { |env| h(env[:name]) }
- # end
- # SafeComponent.call(name: '<h1>hi</h1>') #=> <h1>hi</h1>
- def h(obj)
- CGI.escapeHTML(obj.to_s)
+ # Reek hates this DuplicateMethodCall, but fixing it would mean checking
+ # arity at runtime, rather than when the render macro is called.
+ if arity.zero?
+ define_method(:render) { |&child| _rc_render(&child) }
+ else
+ define_method(:render) { |&child| _rc_render(env, &child) }
+ end
+ end
+
+ def configure_template(options)
+ renderer = Renderer.new(options)
+ define_method(:render) { |&child| renderer.call(self, &child) }
+ end
end
+
+ extend Methods
end
end