require_relative 'component/component_cache'
module Rack
# Subclass Rack::Component to compose declarative, component-based responses
# to HTTP requests
class Component
VERSION = '0.2.0'.freeze
EMPTY = ''.freeze # components render an empty body by default
attr_reader :props
# Initialize a new component with the given props and #render() it.
#
# @example render a HelloWorld component
# class HelloWorld < Rack::Component
# def world
# props[:world]
# end
#
# def call
# %(
Hello #{world}
)
# end
# end
#
# MyComponent.call(world: 'Earth') #=> 'Hello Earth
'
# @return [String, Object] the output of instance#render
def self.call(*props, &block)
new(*props).render(&block)
end
def initialize(props = {})
@props = props
end
# Override call to make your component do work.
# @return [String, Object] usually a string, but really whatever
def render
block_given? ? yield(self) : EMPTY
end
# Rack::Component::Memoized is just like Component, only it
# caches its rendered output in memory and only rerenders
# when called with new props.
class Memoized < self
CACHE_SIZE = 100 # limit cache to 100 keys by default so we don't leak RAM
# instantiate a class-level cache if necessary
# @return [Rack::Component::ComponentCache] a threadsafe in-memory cache
def self.cache
@cache ||= ComponentCache.new(const_get(:CACHE_SIZE))
end
# @example render a Memoized Component
# class Expensive < Rack::Component::Memoized
# def work
# sleep 5
# "#{props[:id]} was expensive"
# end
#
# def call
# %(#{work}
)
# end
# end
#
# # first call takes five seconds
# Expensive.call(id: 1) #=> 1 was expensive
# # subsequent calls with identical props are instant
#
# # subsequent calls with _different_ props take five seconds
# Expensive.call(id: 2) #=> 2 was expensive
#
# @return [String, Object] the cached (or computed) output of render
def self.call(*props, &block)
memoized(*props) { super }
end
# Check the class-level cache, set it to &miss if nil.
# @return [Object] the output of &miss.call
def self.memoized(*props, &miss)
cache.fetch(key(*props), &miss)
end
# @return [Integer] a cache key for this component
def self.key(*props)
props.hash
end
# Clear the cache of each descendant class.
# Generally you'll call this on Rack::Component::Memoized directly.
# @example Clear all caches:
# Rack::Component::Memoized.clear_caches
def self.clear_caches
ObjectSpace.each_object(singleton_class) do |descendant|
descendant.cache.flush
end
end
end
end
end