require_relative 'component/version'
require_relative 'component/memory_cache'
module Rack
# Subclass Rack::Component to compose functional, declarative responses to
# HTTP requests.
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
#
#
#
# #{env[:title]}
#
# #{child.call}
#
# HTML
# end
# end
#
# Layout.call(title: 'Hello') { "Hello from Rack::Component" } #=>
# #
# #
# #
# # Hello
# #
# # Hello from Rack::Component
# #
def call(env = {}, &child)
new(env).call env, &child
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) }
end
# Forget all memoized calls to this component.
def flush
cache.flush
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
# 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))
end
end
def initialize(env = {})
@env = env
end
# 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
attr_reader :env
end
end