rsx-rb 0.1.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.
@@ -0,0 +1,212 @@
1
+ # frozen_string_literal: true
2
+
3
+ module RSX
4
+ # Base class for every `component` defined in a .rsx file.
5
+ #
6
+ # component Greeting do |name:|
7
+ # return <p>Hello {name}</p>
8
+ # end
9
+ #
10
+ # compiles to a subclass whose `rsx_render` is the block body. Components are
11
+ # plain Ruby objects: one instance per render, no inheritance requirements on
12
+ # the caller, and no framework needed to use them.
13
+ class Component
14
+ EMPTY_PROPS = {}.freeze
15
+
16
+ class << self
17
+ attr_accessor :rsx_source_path, :rsx_source_digest
18
+ attr_writer :rsx_cache_options
19
+
20
+ def rsx_cache_options
21
+ @rsx_cache_options
22
+ end
23
+
24
+ # Marks a component whose output never varies. The compiler sets this
25
+ # automatically when a component body is nothing but static markup.
26
+ def rsx_static!
27
+ @rsx_static = true
28
+ end
29
+
30
+ def rsx_static?
31
+ @rsx_static ? true : false
32
+ end
33
+
34
+ # Enables whole-component caching.
35
+ #
36
+ # cache expires_in: 300
37
+ # cache key: ->(props) { [props[:user], props[:locale]] }
38
+ def cache(options = true)
39
+ @rsx_cache_options = RSX.normalize_cache_options(options)
40
+ end
41
+
42
+ # Renders the component. This is the entry point used by compiled markup.
43
+ def rsx_call(props = nil, parent = nil)
44
+ return (@rsx_static_output ||= new(props, parent).rsx_perform) if rsx_static?
45
+
46
+ options = rsx_cache_options
47
+ return new(props, parent).rsx_perform unless options
48
+
49
+ key = rsx_cache_key(props, options)
50
+ cached = RSX.cache.fetch(key, expires_in: options[:expires_in]) do
51
+ new(props, parent).rsx_perform.to_s
52
+ end
53
+ cached.is_a?(SafeString) ? cached : SafeString.new(cached)
54
+ end
55
+
56
+ # Public API: Button.call(label: "Save") => SafeString
57
+ def call(**props)
58
+ rsx_call(props)
59
+ end
60
+
61
+ def render(**props)
62
+ rsx_call(props)
63
+ end
64
+
65
+ def rsx_cache_key(props, options)
66
+ custom = options[:key]
67
+ payload =
68
+ if custom.nil?
69
+ (props || EMPTY_PROPS).reject { |key, _| key == :children }
70
+ elsif custom.respond_to?(:arity) && custom.arity.zero?
71
+ custom.call
72
+ else
73
+ custom.call(props || EMPTY_PROPS)
74
+ end
75
+
76
+ "rsx/#{name || "component"}/#{rsx_source_digest || "0"}/#{RSX.stable_key(payload)}"
77
+ end
78
+
79
+ def rsx_render_style
80
+ return @rsx_render_style if defined?(@rsx_render_style)
81
+
82
+ parameters = rsx_render_parameters
83
+ keyword = parameters.any? { |kind, _| %i[key keyreq keyrest].include?(kind) }
84
+ @rsx_render_style = keyword ? :keyword : :positional
85
+ end
86
+
87
+ def rsx_render_parameters
88
+ instance_method(:rsx_render).parameters
89
+ rescue NameError
90
+ []
91
+ end
92
+
93
+ # The keyword props this component declares.
94
+ def rsx_accepted_props
95
+ @rsx_accepted_props ||= rsx_render_parameters.filter_map do |kind, name|
96
+ name if %i[key keyreq].include?(kind)
97
+ end
98
+ end
99
+
100
+ def rsx_accepts_extra_props?
101
+ return @rsx_accepts_extra_props if defined?(@rsx_accepts_extra_props)
102
+
103
+ @rsx_accepts_extra_props = rsx_render_parameters.any? { |kind, _| kind == :keyrest }
104
+ end
105
+
106
+ def rsx_filter_props(props)
107
+ return EMPTY_PROPS if props.nil? || props.empty?
108
+ return props if rsx_accepts_extra_props?
109
+
110
+ accepted = rsx_accepted_props
111
+ unknown = props.keys - accepted - [:children]
112
+ unless unknown.empty?
113
+ raise PropsError, "#{name} does not accept #{unknown.map(&:inspect).join(", ")}. " \
114
+ "Declared props: #{accepted.map(&:inspect).join(", ")}. " \
115
+ "Add `**rest` to the component parameters to accept anything else."
116
+ end
117
+
118
+ accepted.include?(:children) ? props : props.except(:children)
119
+ end
120
+
121
+ def inspect
122
+ name || super
123
+ end
124
+ end
125
+
126
+ attr_reader :props
127
+
128
+ def initialize(props = nil, parent = nil)
129
+ @props = props || EMPTY_PROPS
130
+ @rsx_parent = parent
131
+ end
132
+
133
+ def rsx_perform
134
+ result =
135
+ if self.class.rsx_render_style == :keyword
136
+ rsx_render(**self.class.rsx_filter_props(@props))
137
+ else
138
+ rsx_render(@props)
139
+ end
140
+ RSX.child(result)
141
+ rescue ArgumentError => e
142
+ raise unless e.message.start_with?("missing keyword", "unknown keyword", "wrong number of arguments")
143
+
144
+ raise PropsError, "#{self.class.name}: #{e.message}. " \
145
+ "Declared props: #{self.class.rsx_accepted_props.map(&:inspect).join(", ")}"
146
+ end
147
+
148
+ # The markup nested inside this component's tag, rendered on demand.
149
+ def children
150
+ @props[:children]
151
+ end
152
+
153
+ def children?
154
+ child = children
155
+ !child.nil? && !(child.respond_to?(:empty?) && child.empty?)
156
+ end
157
+
158
+ # The object that rendered this component: a view context in Rails, the
159
+ # parent component when nested, or nil when rendered directly.
160
+ def rsx_parent
161
+ @rsx_parent
162
+ end
163
+
164
+ # The nearest non-component render context, i.e. the Rails view. Gives access
165
+ # to url helpers, form builders, `t`, asset helpers and anything else the
166
+ # application exposes to templates.
167
+ def helpers
168
+ node = @rsx_parent
169
+ node = node.rsx_parent while node.is_a?(Component)
170
+ return node unless node.nil?
171
+
172
+ raise Error, "#{self.class.name} has no view context. Render it from a Rails view, " \
173
+ "or pass one with RSX.render(#{self.class.name}, context: view)."
174
+ end
175
+ alias view_context helpers
176
+
177
+ def helpers?
178
+ node = @rsx_parent
179
+ node = node.rsx_parent while node.is_a?(Component)
180
+ !node.nil?
181
+ end
182
+
183
+ # Reads a value provided by an enclosing <Ctx.Provider>.
184
+ def use_context(context)
185
+ context.value
186
+ end
187
+
188
+ # Caches a fragment of markup.
189
+ #
190
+ # {cache(["sidebar", user.id], expires_in: 300) do
191
+ # <nav>...</nav>
192
+ # end}
193
+ def cache(key, expires_in: nil)
194
+ full_key = "rsx/#{self.class.name}/#{self.class.rsx_source_digest || "0"}/#{RSX.stable_key(key)}"
195
+ cached = RSX.cache.fetch(full_key, expires_in: expires_in) { RSX.child(yield).to_s }
196
+ cached.is_a?(SafeString) ? cached : SafeString.new(cached)
197
+ end
198
+
199
+ # Escape hatch for trusted HTML built elsewhere.
200
+ def raw(value)
201
+ RSX.raw(value)
202
+ end
203
+
204
+ def to_s
205
+ rsx_perform
206
+ end
207
+
208
+ def inspect
209
+ "#<#{self.class.name} #{@props.inspect}>"
210
+ end
211
+ end
212
+ end
@@ -0,0 +1,67 @@
1
+ # frozen_string_literal: true
2
+
3
+ module RSX
4
+ # React's Context API: a value provided high in the tree and read anywhere
5
+ # below it without threading props through every component in between.
6
+ #
7
+ # Theme = RSX.create_context("light")
8
+ #
9
+ # <Theme.Provider value={"dark"}>
10
+ # <Toolbar />
11
+ # </Theme.Provider>
12
+ #
13
+ # # inside any descendant
14
+ # {use_context(Theme)}
15
+ #
16
+ # Provided values live on a per-thread stack, so concurrent requests never see
17
+ # each other's context.
18
+ class Context
19
+ attr_reader :name, :default
20
+
21
+ def initialize(default = nil, name: nil)
22
+ @default = default
23
+ @name = name
24
+ @key = :"rsx_context_#{object_id}"
25
+ end
26
+
27
+ def value
28
+ stack = Thread.current[@key]
29
+ stack && !stack.empty? ? stack.last : @default
30
+ end
31
+ alias current value
32
+
33
+ def with(value)
34
+ stack = (Thread.current[@key] ||= [])
35
+ stack.push(value)
36
+ yield
37
+ ensure
38
+ stack.pop
39
+ end
40
+
41
+ # Allows <Theme.Provider value={...}>
42
+ def Provider # rubocop:disable Naming/MethodName
43
+ @provider ||= ProviderComponent.new(self)
44
+ end
45
+ alias provider Provider
46
+
47
+ def inspect
48
+ "#<RSX::Context #{name || object_id} default=#{@default.inspect}>"
49
+ end
50
+
51
+ # A component-like object: anything answering to rsx_call can be rendered.
52
+ class ProviderComponent
53
+ def initialize(context)
54
+ @context = context
55
+ end
56
+
57
+ def name
58
+ "#{@context.name || "Context"}.Provider"
59
+ end
60
+
61
+ def rsx_call(props = nil, _parent = nil)
62
+ props ||= {}
63
+ @context.with(props[:value]) { RSX.child(props[:children]) }
64
+ end
65
+ end
66
+ end
67
+ end
data/lib/rsx/errors.rb ADDED
@@ -0,0 +1,27 @@
1
+ # frozen_string_literal: true
2
+
3
+ module RSX
4
+ class Error < StandardError; end
5
+
6
+ # Raised when a .rsx file cannot be transformed into Ruby.
7
+ class SyntaxError < Error
8
+ attr_reader :path, :line, :column
9
+
10
+ def initialize(message, path: nil, line: nil, column: nil)
11
+ @path = path
12
+ @line = line
13
+ @column = column
14
+ location = [path || "(rsx)", line, column].compact.join(":")
15
+ super("#{location}: #{message}")
16
+ end
17
+ end
18
+
19
+ # Raised when a <Tag /> cannot be resolved to a component.
20
+ class UnknownComponentError < Error; end
21
+
22
+ # Raised when a component is rendered with props it does not accept.
23
+ class PropsError < Error; end
24
+
25
+ # Raised when RSX cannot locate a .rsx file for a path or import.
26
+ class FileNotFoundError < Error; end
27
+ end
data/lib/rsx/escape.rb ADDED
@@ -0,0 +1,59 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "cgi/escape"
4
+
5
+ module RSX
6
+ # HTML escaping. Runtime escaping goes through CGI.escapeHTML (a C extension
7
+ # in stdlib) so no third-party gem is needed.
8
+ module Escape
9
+ # Matches a character reference such as &amp; &#169; or &#x2014;
10
+ ENTITY = /&(?:[a-zA-Z][a-zA-Z0-9]{1,30}|#[0-9]{1,7}|#[xX][0-9a-fA-F]{1,6});/
11
+
12
+ module_function
13
+
14
+ # Escapes text for element content or an attribute value.
15
+ def html(string)
16
+ CGI.escapeHTML(string)
17
+ end
18
+
19
+ # True for values that must not be escaped again.
20
+ #
21
+ # ActiveSupport::SafeBuffer (what Rails helpers return) is a String
22
+ # subclass, so the exact-class check below is what keeps `link_to` output
23
+ # from being escaped while still taking the fast path for plain strings.
24
+ def safe?(value)
25
+ return true if value.is_a?(SafeString)
26
+ return false if value.instance_of?(String)
27
+
28
+ value.respond_to?(:html_safe?) && value.html_safe?
29
+ end
30
+
31
+ # Escaping for *static* text baked in at compile time.
32
+ #
33
+ # JSX passes character references such as &nbsp; through to the browser, so
34
+ # RSX keeps well-formed entities intact while still escaping stray markup.
35
+ def static_text(string)
36
+ return CGI.escapeHTML(string) unless string.include?("&")
37
+
38
+ out = +""
39
+ last = 0
40
+ string.scan(ENTITY) do
41
+ match = Regexp.last_match
42
+ out << CGI.escapeHTML(string[last...match.begin(0)])
43
+ out << match[0]
44
+ last = match.end(0)
45
+ end
46
+ out << CGI.escapeHTML(string[last..]) if last < string.length
47
+ out
48
+ end
49
+
50
+ # Escapes a value destined for a double-quoted attribute.
51
+ def attribute(value)
52
+ return value if value.is_a?(SafeString)
53
+ return CGI.escapeHTML(value) if value.instance_of?(String)
54
+ return value.to_s if safe?(value)
55
+
56
+ CGI.escapeHTML(value.to_s)
57
+ end
58
+ end
59
+ end
@@ -0,0 +1,29 @@
1
+ # frozen_string_literal: true
2
+
3
+ module RSX
4
+ # Mixed into ActionView so components can be rendered from ERB, Haml, Slim or
5
+ # anywhere else a view helper is available.
6
+ #
7
+ # <%= rsx UserProfile, user: @user %>
8
+ # <%= rsx "Card", title: "Hello" do %>
9
+ # <p>Body rendered by ERB, passed to the component as children.</p>
10
+ # <% end %>
11
+ module Helpers
12
+ def rsx(target, **props, &block)
13
+ children =
14
+ if block
15
+ captured = capture(&block)
16
+ RSX::Children.new { RSX.raw(captured) }
17
+ end
18
+
19
+ component = target.is_a?(String) || target.is_a?(Symbol) ? RSX.lookup_component(target) : target
20
+ RSX.safe(RSX.render_component(component, props.empty? ? nil : props, children, self))
21
+ end
22
+ alias render_rsx rsx
23
+
24
+ # Renders a .rsx file directly by path, relative to the configured paths.
25
+ def rsx_file(path, **props)
26
+ RSX.render_file(path, context: self, **props)
27
+ end
28
+ end
29
+ end
data/lib/rsx/loader.rb ADDED
@@ -0,0 +1,194 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "monitor"
4
+
5
+ module RSX
6
+ # Finds, compiles, evaluates and reloads .rsx files.
7
+ class Loader
8
+ EXTENSIONS = [".rsx", ".html.rsx"].freeze
9
+
10
+ Entry = Struct.new(:path, :digest, :mtime, :components, :default, :template, keyword_init: true) do
11
+ # Markup files render through a template; component files render their
12
+ # default export.
13
+ def renderable
14
+ default || components.first
15
+ end
16
+ end
17
+
18
+ def initialize(config)
19
+ @config = config
20
+ @entries = {}
21
+ @stack = []
22
+ @monitor = Monitor.new
23
+ end
24
+
25
+ def entries
26
+ @entries.values
27
+ end
28
+
29
+ def loaded?(path)
30
+ @entries.key?(File.expand_path(path))
31
+ end
32
+
33
+ # The file currently being evaluated, used to attribute components to it.
34
+ def current_entry
35
+ @stack.last
36
+ end
37
+
38
+ def compile_cache
39
+ @config.compile_cache
40
+ end
41
+
42
+ # Compiles and evaluates a .rsx file exactly once per digest.
43
+ def load(path, force: false)
44
+ absolute = File.expand_path(path)
45
+ raise FileNotFoundError, "no such RSX file: #{absolute}" unless File.file?(absolute)
46
+
47
+ @monitor.synchronize do
48
+ source = File.read(absolute)
49
+ digest = compile_cache.digest(source)
50
+ existing = @entries[absolute]
51
+ return existing if existing && existing.digest == digest && !force
52
+
53
+ unload(existing) if existing
54
+
55
+ entry = Entry.new(path: absolute, digest: digest, mtime: mtime(absolute), components: [], default: nil)
56
+ @entries[absolute] = entry
57
+ @stack.push(entry)
58
+
59
+ begin
60
+ ruby = compile_cache.fetch(absolute, source) do
61
+ Transformer.transform(source, path: absolute)
62
+ end
63
+
64
+ if Transformer.defines_components?(ruby)
65
+ # Component files are evaluated at the top level so that `class`,
66
+ # `def` and constants behave exactly as they do in a .rb file.
67
+ eval(ruby, TOPLEVEL_BINDING, absolute, 1) # rubocop:disable Security/Eval
68
+ else
69
+ entry.template = Template.from_ruby(ruby, path: absolute, digest: digest)
70
+ end
71
+ rescue Exception # rubocop:disable Lint/RescueException
72
+ @entries.delete(absolute)
73
+ raise
74
+ ensure
75
+ @stack.pop
76
+ end
77
+
78
+ entry
79
+ end
80
+ end
81
+
82
+ # Loads every .rsx file under the configured paths. Called at boot in
83
+ # production so no request pays for compilation.
84
+ def load_all(paths = @config.paths)
85
+ files(paths).each { |file| load(file) }
86
+ end
87
+
88
+ def files(paths = @config.paths)
89
+ Array(paths).flat_map do |root|
90
+ next [] unless File.directory?(root)
91
+
92
+ Dir.glob(File.join(root, "**", "*.rsx")).sort
93
+ end
94
+ end
95
+
96
+ # Reloads only the files whose contents changed. Used by the Rails reloader.
97
+ def reload!
98
+ @monitor.synchronize do
99
+ @entries.values.each do |entry|
100
+ if !File.file?(entry.path)
101
+ unload(entry)
102
+ @entries.delete(entry.path)
103
+ elsif mtime(entry.path) != entry.mtime
104
+ load(entry.path, force: true)
105
+ end
106
+ end
107
+ end
108
+ end
109
+
110
+ def clear
111
+ @monitor.synchronize do
112
+ @entries.each_value { |entry| unload(entry) }
113
+ @entries.clear
114
+ end
115
+ end
116
+
117
+ # Resolves an import specifier or template path to a file on disk.
118
+ def resolve(spec, from: nil)
119
+ spec = spec.to_s
120
+ candidates = []
121
+
122
+ if spec.start_with?("/")
123
+ candidates << spec
124
+ else
125
+ candidates << File.expand_path(spec, File.dirname(from)) if from && !from.empty?
126
+ Array(@config.paths).each { |root| candidates << File.join(root, spec) }
127
+ candidates << File.expand_path(spec, Dir.pwd)
128
+ end
129
+
130
+ candidates.each do |candidate|
131
+ return candidate if File.file?(candidate)
132
+
133
+ EXTENSIONS.each do |extension|
134
+ with_extension = "#{candidate}#{extension}"
135
+ return with_extension if File.file?(with_extension)
136
+ end
137
+ end
138
+
139
+ nil
140
+ end
141
+
142
+ def resolve!(spec, from: nil)
143
+ resolve(spec, from: from) ||
144
+ raise(FileNotFoundError, "could not find `#{spec}`#{" imported from #{from}" if from}. " \
145
+ "Looked in: #{Array(@config.paths).join(", ")}")
146
+ end
147
+
148
+ def import(spec, as: nil, from: nil)
149
+ entry = load(resolve!(spec, from: from))
150
+ Array(as).each { |name| alias_constant(name, entry) }
151
+ entry.default || entry.components.first
152
+ end
153
+
154
+ # Records a component defined by the file currently being loaded.
155
+ def track(component)
156
+ entry = current_entry
157
+ return component unless entry
158
+
159
+ entry.components << component
160
+ entry.default ||= component
161
+ component.rsx_source_path = entry.path
162
+ component.rsx_source_digest = entry.digest
163
+ component
164
+ end
165
+
166
+ def default_export(path)
167
+ entry = load(path)
168
+ entry.default || entry.components.first
169
+ end
170
+
171
+ private
172
+
173
+ def mtime(path)
174
+ File.mtime(path)
175
+ rescue SystemCallError
176
+ nil
177
+ end
178
+
179
+ def alias_constant(name, entry)
180
+ name = name.to_s
181
+ target = entry.default || entry.components.first
182
+ return if target.nil?
183
+ return if RSX.const_defined_at?(name)
184
+
185
+ RSX.assign_constant(name, target)
186
+ end
187
+
188
+ def unload(entry)
189
+ entry.components.each { |component| RSX.remove_constant(component) }
190
+ entry.components.clear
191
+ entry.default = nil
192
+ end
193
+ end
194
+ end
data/lib/rsx/nodes.rb ADDED
@@ -0,0 +1,26 @@
1
+ # frozen_string_literal: true
2
+
3
+ module RSX
4
+ # The RSX syntax tree. Every node records the source line it started on so the
5
+ # generated Ruby can be padded to keep backtraces aligned with the .rsx file.
6
+ module Nodes
7
+ # Literal markup text, already whitespace-normalized JSX style.
8
+ Text = Struct.new(:value, :line)
9
+
10
+ # {ruby} in child position.
11
+ Expression = Struct.new(:source, :line)
12
+
13
+ # <div>, <img />, <my-element>
14
+ Element = Struct.new(:tag, :attributes, :children, :self_closing, :line)
15
+
16
+ # <Button>, <Admin::Card>, <Theme.Provider>
17
+ Component = Struct.new(:name, :attributes, :children, :line)
18
+
19
+ # <>...</>
20
+ Fragment = Struct.new(:children, :line)
21
+
22
+ # kind is :static (name="text"), :expression (name={ruby}),
23
+ # :boolean (bare name) or :spread ({**ruby} / {...ruby}).
24
+ Attribute = Struct.new(:name, :value, :kind, :line)
25
+ end
26
+ end
@@ -0,0 +1,81 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "template_handler"
4
+ require_relative "helpers"
5
+
6
+ module RSX
7
+ # Wires RSX into a Rails application. Nothing here runs unless Rails is
8
+ # already loaded, which keeps the gem usable outside Rails.
9
+ #
10
+ # Configure it from config/application.rb:
11
+ #
12
+ # config.rsx.paths = [Rails.root.join("app/components")]
13
+ # config.rsx.cache_store = :rails
14
+ #
15
+ class Railtie < ::Rails::Railtie
16
+ config.rsx = ActiveSupport::OrderedOptions.new
17
+
18
+ initializer "rsx.configure" do |app|
19
+ options = app.config.rsx
20
+
21
+ RSX.configure do |config|
22
+ config.paths = Array(options.paths.presence || default_paths(app))
23
+ config.cache_dir = options.key?(:cache_dir) ? options.cache_dir : app.root.join("tmp/cache/rsx").to_s
24
+ config.component_namespace = options.component_namespace || Object
25
+ config.reload = options.key?(:reload) ? options.reload : reloading?(app)
26
+ config.cache_store = build_cache_store(options.cache_store) if options.cache_store
27
+ end
28
+ end
29
+
30
+ initializer "rsx.action_view" do
31
+ ActiveSupport.on_load(:action_view) do
32
+ ActionView::Template.register_template_handler(:rsx, RSX::TemplateHandler)
33
+ include RSX::Helpers
34
+ end
35
+ end
36
+
37
+ initializer "rsx.load_components" do |app|
38
+ # Eager load in production so no request ever pays for compilation, and
39
+ # reload changed files between requests in development.
40
+ if app.config.eager_load
41
+ app.config.after_initialize { RSX.load_all }
42
+ else
43
+ app.reloader.to_prepare { RSX.reload! if RSX.config.reload }
44
+ end
45
+
46
+ app.config.watchable_dirs ||= {}
47
+ Array(RSX.config.paths).each do |path|
48
+ app.config.watchable_dirs[path.to_s] = [:rsx] if File.directory?(path.to_s)
49
+ end
50
+ end
51
+
52
+ rake_tasks do
53
+ load File.expand_path("tasks.rake", __dir__)
54
+ end
55
+
56
+ # Initializer blocks are instance_exec'd on the railtie instance, so these
57
+ # helpers have to be instance methods.
58
+ private
59
+
60
+ def default_paths(app)
61
+ %w[app/components app/rsx].map { |path| app.root.join(path).to_s }
62
+ end
63
+
64
+ def reloading?(app)
65
+ if app.config.respond_to?(:enable_reloading)
66
+ app.config.enable_reloading
67
+ else
68
+ !app.config.cache_classes
69
+ end
70
+ end
71
+
72
+ def build_cache_store(setting)
73
+ case setting
74
+ when :rails, "rails" then Cache::Rails.new
75
+ when :memory, "memory" then Cache::Memory.new
76
+ when :null, "null", false then Cache::Null.new
77
+ else setting
78
+ end
79
+ end
80
+ end
81
+ end