Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
25 changes: 25 additions & 0 deletions lib/enumerize/attribute.rb
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,11 @@ def initialize(klass, name, options={})
end

@skip_validations_value = options.fetch(:skip_validations, false)

# Lazily populated cache of i18n lookup keys, keyed by value name. Values
# whose #text is never rendered never build (or retain) their keys, while
# rendered values build them once and reuse them on subsequent calls.
@i18n_keys_cache = {}
end

def find_default_value(value)
Expand Down Expand Up @@ -63,6 +68,26 @@ def i18n_scopes
end
end

# Returns the cached i18n lookup keys for +value+, building them via the
# given block on a miss. Memoizing here (rather than on the frozen Value)
# means a value's keys are composed at most once, recovering the cost of
# rebuilding them on every #text call, while values whose #text is never
# rendered never build or retain any keys.
#
# The cache is updated copy-on-write: readers always see a fully built hash
# and never a half-mutated one, so concurrent #text calls stay safe without
# locking. A race between two builds is last-writer-wins — the result is
# always correct; at worst a clobbered entry is rebuilt on its next call.
def i18n_keys(value)
key = value.to_s
cache = @i18n_keys_cache
cache[key] || begin
keys = yield
@i18n_keys_cache = cache.merge(key => keys)
keys
end
end

def options(options = {})
values = if options.empty?
@values
Expand Down
28 changes: 17 additions & 11 deletions lib/enumerize/value.rb
Original file line number Diff line number Diff line change
Expand Up @@ -14,20 +14,11 @@ def initialize(attr, name, value=nil)
@value = value.nil? ? name.to_s : value

super(name.to_s)

@i18n_keys = @attr.i18n_scopes.map do |s|
scope = Utils.call_if_callable(s, @value)

:"#{scope}.#{self}"
end
@i18n_keys << :"enumerize.defaults.#{@attr.name}.#{self}"
@i18n_keys << :"enumerize.#{@attr.name}.#{self}"
@i18n_keys << ActiveSupport::Inflector.humanize(ActiveSupport::Inflector.underscore(self)) # humanize value if there are no translations
@i18n_keys
end

def text
I18n.t(@i18n_keys[0], :default => @i18n_keys[1..-1]) if @i18n_keys
keys = @attr.i18n_keys(self) { build_i18n_keys }
I18n.t(keys[0], :default => keys[1..-1])
end

def ==(other)
Expand All @@ -44,6 +35,21 @@ def as_json(*)

private

# Composes the ordered i18n lookup keys for this value. Invoked by the
# attribute only on a cache miss (see Attribute#i18n_keys), so values whose
# +text+ is never rendered never build or retain their keys.
def build_i18n_keys
keys = @attr.i18n_scopes.map do |s|
scope = Utils.call_if_callable(s, @value)

:"#{scope}.#{self}"
end
keys << :"enumerize.defaults.#{@attr.name}.#{self}"
keys << :"enumerize.#{@attr.name}.#{self}"
keys << ActiveSupport::Inflector.humanize(ActiveSupport::Inflector.underscore(self)) # humanize value if there are no translations
keys
end

def predicate_call(value)
value == self
end
Expand Down
23 changes: 23 additions & 0 deletions test/attribute_test.rb
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,29 @@ def build_attr(*args, &block)
end
end

describe 'i18n keys' do
it 'builds the keys via the block on a miss' do
build_attr nil, 'foo', :in => %w[a b]
keys = attr.i18n_keys(attr.find_value('a')) { [:built] }
expect(keys).must_equal [:built]
end

it 'memoizes per value and does not rebuild on a hit' do
build_attr nil, 'foo', :in => %w[a b]
first = attr.i18n_keys(attr.find_value('a')) { [:built] }
second = attr.i18n_keys(attr.find_value('a')) { raise 'should not rebuild' }
expect(second).must_be_same_as first
end

it 'caches each value independently' do
build_attr nil, 'foo', :in => %w[a b]
a_keys = attr.i18n_keys(attr.find_value('a')) { [:a] }
b_keys = attr.i18n_keys(attr.find_value('b')) { [:b] }
expect(a_keys).must_equal [:a]
expect(b_keys).must_equal [:b]
end
end

describe 'arguments' do
it 'returns arguments' do
build_attr nil, :foo, :in => [:a, :b], :scope => true
Expand Down
5 changes: 5 additions & 0 deletions test/value_test.rb
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,11 @@ class Attr < Struct.new(:values, :name, :i18n_scopes, :klass)
def value?(value)
values.include?(value)
end

# Caching is the real Attribute's concern; the double just composes.
def i18n_keys(_value)
yield
end
end

let(:attr) { Attr.new([], "attribute_name", [], Model) }
Expand Down