diff --git a/lib/enum.rb b/lib/enum.rb index f0dc84a..8832902 100644 --- a/lib/enum.rb +++ b/lib/enum.rb @@ -2,3 +2,4 @@ require 'enum/token_not_found_error' require 'enum/base' require 'enum/predicates' +require 'enum/value' diff --git a/lib/enum/base.rb b/lib/enum/base.rb index e5d7f33..76ea5a3 100644 --- a/lib/enum/base.rb +++ b/lib/enum/base.rb @@ -1,4 +1,5 @@ require 'set' +require 'forwardable' module Enum class Base < BasicObject @@ -37,13 +38,30 @@ def enum(t) ts end - def name(t) - translate(enum(t)) + # Allow Class.name to work if no args are given. + def name(*t) + if t.empty? + super + else + translate(enum(t[0])) + end end def index(token) history.index(enum(token)) end + + # Render value given interger, string, or symbol. + def [](val) + case + when val.is_a?(::Integer) + self.all[val] + when val.is_a?(::Symbol) || val.is_a?(::String) + self.enum(val) + else + self.enum(val) + end + end protected @@ -83,6 +101,13 @@ def add_value(val) end def init_child_class(child) + class << child + extend ::Forwardable + def_delegators :'self::Value', :default_value, :suppress_read_errors, :new + end + child.const_set :Value, ::Class.new(::Enum::Value) + child::Value.klass = child + child.store = self.store.clone child.history = self.history.clone end diff --git a/lib/enum/value.rb b/lib/enum/value.rb new file mode 100644 index 0000000..27317f1 --- /dev/null +++ b/lib/enum/value.rb @@ -0,0 +1,149 @@ +module Enum + class Value + + # TODO: Perhaps stored_value should be renamed unsafe_value? + + include Comparable + + attr_reader :stored_value, :error + + class << self + #attr_accessor :default_value, :suppress_read_errors, :klass + attr_writer :default_value, :suppress_read_errors + attr_accessor :klass + end + + def self.inherited(subclass) + # Value subclass settings and options + # @default_value => <:$error|:$any|:something|nil> + # @suppress_read_errors => + subclass.suppress_read_errors = false + subclass.default_value = :$error + super + end + + # Combined getter/setter for 'default_value'. + def self.default_value(*args) + # Can't use .any? because [nil].any? is false (ruby 2.4), + # and nil is a valid argument here. + # [nil].empty? is also false, which is what we want. + if args.empty? + @default_value + else + @default_value = args[0] + end + end + + # Combined getter/setter for 'suppress_read_errors'. + def self.suppress_read_errors(*args) + if !args.empty? + @suppress_read_errors = args[0] + else + @suppress_read_errors + end + end + + # Load a primitive (symbol, string, integer) into new enum Value instance, + # taking into consideration enum constraints, default_value setting. + # Returns frozen Value instance. + # TODO: Add param: opts = {} for run-time temp settings changes (default_value, suppress_read_errors). + # TODO: Maybe convert all opts to @options => {default_value:, suppress_read_errors:} + # This would be easier to maintain but would require some special getter/setter methods. + def initialize(raw_val, opts={}) + begin + @stored_value = klass[raw_val].to_sym.freeze + rescue Enum::TokenNotFoundError => _error + @error = _error.freeze + case + when self.class.default_value == :$error || opts[:default_value] == :$error + raise _error + when self.class.default_value == :$any || opts[:default_value] == :$any + @stored_value = raw_val.freeze + else + @stored_value = self.class.default_value.freeze + end + end + self.freeze + self + end + + # Returns the Enum::Base subclass attached through Enum::Value subclass. + def klass + self.class.klass + end + + # Convenience method to pass arguments to klass[]. + # Returns enum value given symbol, string, or integer. + def enum_value(_value=stored_value) + begin + klass[_value] + rescue Enum::TokenNotFoundError => _error + if self.class.suppress_read_errors + _value + else + raise _error + end + end + end + private :enum_value + + # Returns result of Enum::Base subclass.enum(self) as string. + def to_s + enum_value.to_s + end + alias_method :to_str, :to_s + + # Returns result of Enum::Base subclass.enum(self) as symbol. + def to_sym + val = enum_value + val.to_sym if val.respond_to?(:to_sym) + end + + # Returns result of Enum::Base subclass.enum(self). + def value + enum_value + end + + # Is stored value nil? + def nil? + @stored_value.nil? + end + + # Is stored value valid for this enum class? + def valid? + klass.enum(@stored_value) + true + rescue Enum::TokenNotFoundError + false + end + + # This (or other) value's enum index. + def index(_value=stored_value) + begin + klass.index(_value) + rescue Enum::TokenNotFoundError => _error + raise _error unless self.class.suppress_read_errors + end + end + alias_method :to_i, :index + + # Enable comparisons between Value instances, strings, symbols, and integers. + def <=>(other) + case + when other.is_a?(Symbol) + index <=> index(other) + when other.is_a?(String) + index <=> index(other) + when other.is_a?(Integer) + index <=> other + when other.is_a?(self.class) && other.klass == klass + index <=> other.index + else + # Nil will be uncomparable with strings, symbols, or integers, + # and will raise exception, as it should. + nil + end + end + + end # Value +end # Enum \ No newline at end of file diff --git a/test/base_test.rb b/test/base_test.rb index 8c759ad..fb9fdc0 100644 --- a/test/base_test.rb +++ b/test/base_test.rb @@ -1,4 +1,4 @@ -require 'test_helper' +require_relative 'test_helper' describe Enum::Base do describe Side do diff --git a/test/predicates_test.rb b/test/predicates_test.rb index 0ccf4ce..4b48335 100644 --- a/test/predicates_test.rb +++ b/test/predicates_test.rb @@ -1,4 +1,4 @@ -require 'test_helper' +require_relative 'test_helper' describe Enum::Predicates do describe Table do diff --git a/test/support/fixtures.rb b/test/support/fixtures.rb index f87dc1e..588b384 100644 --- a/test/support/fixtures.rb +++ b/test/support/fixtures.rb @@ -21,3 +21,14 @@ class Side < Enum::Base values :left, :right end end + +class Suppressed < Enum::Base + values :left, :right, :whole + suppress_read_errors true +end + +class LoadAnyValue < Enum::Base + values :left, :right, :whole + default_value :$any + suppress_read_errors true +end \ No newline at end of file diff --git a/test/value_test.rb b/test/value_test.rb new file mode 100644 index 0000000..3add367 --- /dev/null +++ b/test/value_test.rb @@ -0,0 +1,230 @@ +require_relative 'test_helper' + +describe Enum::Value do + + describe '.inherited' do + describe 'sets subclass.default_value to :$error' do + specify { assert_equal :$error, Class.new(Enum::Value).instance_variable_get(:@default_value) } + end + describe 'sets subclass.suppress_read_errors to false' do + specify { assert_equal false, Class.new(Enum::Value).instance_variable_get(:@suppress_read_errors) } + end + end + + describe '.default_value' do + before { @side = Class.new(Side) } + describe 'sets class-level @default_value to args[0] if args[0] exist' do + specify { @side::Value.default_value(:something); assert_equal :something, @side::Value.instance_variable_get(:@default_value) } + end + describe 'returns class-level @default_value if args.empty?' do + specify { assert_equal :$error, @side::Value.instance_variable_get(:@default_value) } + end + end + + describe '.suppress_read_errors' do + before { @side = Class.new(Side) } + describe 'sets class-level @suppress_read_errors to args[0] if args[0] exist' do + specify { @side::Value.suppress_read_errors(true); assert_equal true, @side::Value.instance_variable_get(:@suppress_read_errors) } + end + describe 'returns class-level @suppress_read_errors if args.empty?' do + specify { assert_equal false, @side::Value.instance_variable_get(:@suppress_read_errors) } + end + end + + describe '#initialize' do + # TODO: test for params opts. + + describe 'always returns frozen object, unless exception raised' do + specify { assert Side::Value.allocate.send(:initialize, :left).frozen? } + end + + describe 'given valid enum token' do + it "sets @stored_value with frozen token" do + assert_equal :left, Side::Value.allocate.send(:initialize, :left).instance_variable_get(:@stored_value) + assert Side::Value.allocate.send(:initialize, :left).instance_variable_get(:@stored_value).frozen? + end + end + + describe 'given invalid enum token' do + before do + @side_class = Class.new(Side) + @val_class = @side_class::Value + @val_class.default_value :$error + #@invalid_val = @val_class.allocate.send(:initialize, :invalid) + end + + it 'sets @error with TokenNotFoundError' do + @val_class.default_value :$any + @invalid_val = @val_class.allocate.send(:initialize, :invalid) + assert_kind_of Enum::TokenNotFoundError, @invalid_val.instance_variable_get(:@error) + end + + describe 'when default_value == :$error' do + it 'raises TokenNotFoundError' do + assert_raises(Enum::TokenNotFoundError) do + @val_class.allocate.send(:initialize, :invalid) + end + end + end + + describe 'when default_value == :$any' do + before do + @val_class.default_value :$any + @invalid_value = @val_class.allocate.send(:initialize, :invalid) + end + it 'sets @stored_value = raw_val.freeze' do + assert_equal :invalid, @invalid_value.instance_variable_get(:@stored_value) + assert @invalid_value.instance_variable_get(:@stored_value).frozen? + end + end + + describe 'when default_value is anything else' do + before do + @val_class.default_value :none + @invalid_value = @val_class.allocate.send(:initialize, :invalid) + end + it 'sets @stored_value = self.class.default_value.freeze' do + assert_equal :none, @invalid_value.instance_variable_get(:@stored_value) + assert @invalid_value.instance_variable_get(:@stored_value).frozen? + end + end + end + end + + describe '#klass' do + describe 'returns Base child' do + specify { assert_equal Side, Side.new(:left).klass } + end + end + + describe '#enum_value' do + describe 'sends symbol or string to Base#enum' do + specify("with_symbol") { assert_equal 'right', Side.new(:right).send(:enum_value, :right) } + specify("with_string") { assert_equal 'right', Side.new(:right).send(:enum_value, 'right') } + end + + describe 'sends integer to Base#index' do + specify { assert_equal 'right', Side.new(:right).send(:enum_value, 1) } + end + + describe 'sends self.stored_value as default' do + specify { assert_equal 'whole', Side.new(:whole).send(:enum_value) } + end + + describe 'wihtout :suppress_read_errors' do + describe 'raises TokenNotFoundError if given invalid token' do + specify do + assert_raises Enum::TokenNotFoundError do + Side.new(:right).send(:enum_value, :invalid) + end + end + end + + describe 'returns nil given out-of-bounds integer' do + specify { assert_nil Side.new(:right).send(:enum_value, 9) } + end + end + + describe 'with :suppress_read_errors' do + describe 'returns any given invalid token' do + specify do + suppressed = Suppressed.new(:right) + assert_equal :invalid, suppressed.send(:enum_value, :invalid) + end + end + end + end # enum_value + + describe '#to_s' do + describe 'gets enum_value as string' do + specify('is_string') { assert_instance_of String, Side.new(:left).send(:to_s) } + specify('return_correct_value') { assert_equal 'left', Side.new(:left).send(:to_s) } + end + end + + describe '#to_sym' do + describe 'gets enum_value as symbol' do + specify('is_symbol') { assert_instance_of Symbol, Side.new(:left).send(:to_sym) } + specify('return_correct_value') { assert_equal :left, Side.new(:left).send(:to_sym) } + end + + describe 'returns nil if enum_value does not repond_to to_sym' do + specify { assert_nil LoadAnyValue.new(nil).send(:to_sym) } + end + end + + describe '#value' do + describe 'gets enum_value of self' do + specify { assert_equal 'left', Side.new(:left).send(:value) } + end + end + + describe '#nil?' do + describe 'returns true if @stored_value is nil' do + specify('given_nil') { assert LoadAnyValue.new(nil).send(:nil?) } + specify('given_non_nil') { assert_equal false, LoadAnyValue.new(:something).send(:nil?) } + end + end + + describe '#valid?' do + describe 'returns true if @stored_value is valid enum' do + specify('given_valid_value') { assert LoadAnyValue.new(:left).send(:valid?) } + specify('given_invalid_value') { assert_equal false, LoadAnyValue.new(nil).send(:valid?) } + end + end + + describe '#index' do + describe 'returns index of given valid token' do + specify { assert_equal 2, Side.new(:left).send(:index, :whole) } + end + + describe 'raises TokenNotFoundError if token invalid' do + specify do + assert_raises Enum::TokenNotFoundError do + Side.new(:left).send(:index, :invalid) + end + end + end + + describe 'returns index of @stored_value' do + specify { assert_equal 1, Side.new(:right).send(:index) } + end + + describe 'returns nil if token invalid with suppress_read_errors' do + specify { assert_nil LoadAnyValue.new(:right).send(:index, :invalid) } + end + end + + describe '#<=>' do + describe 'comparable with symbol' do + specify('less_than') { assert_equal(-1, Side.new(:right) <=> :whole) } + specify('equal_to') { assert_equal(0, Side.new(:right) <=> :right) } + specify('greater_than') { assert_equal(1, Side.new(:right) <=> :left) } + end + + describe 'comparable with string' do + specify('less_than') { assert_equal(-1, Side.new(:right) <=> 'whole') } + specify('equal_to') { assert_equal(0, Side.new(:right) <=> 'right') } + specify('greater_than') { assert_equal(1, Side.new(:right) <=> 'left') } + end + + describe 'comparable with integer' do + specify('less_than') { assert_equal(-1, Side.new(:right) <=> 2) } + specify('equal_to') { assert_equal(0, Side.new(:right) <=> 1) } + specify('greater_than') { assert_equal(1, Side.new(:right) <=> 0) } + end + + describe 'comparable with other Value object of same enum class' do + specify('less_than') { assert_equal(-1, Side.new(:right) <=> Side.new(:whole)) } + specify('equal_to') { assert_equal(0, Side.new(:right) <=> Side.new(:right)) } + specify('greater_than') { assert_equal(1, Side.new(:right) <=> Side.new(:left)) } + end + + describe 'returns nil if uncomarable with other' do + specify('with_object') { assert_nil(Side.new(:right) <=> Object.new) } + specify('with_nil') { assert_nil(Side.new(:right) <=> nil) } + specify('with_invalid_other_instance') { assert_nil(Side.new(:right) <=> LoadAnyValue.new(:right)) } + end + end + +end # Enum::Value \ No newline at end of file