Skip to content

Commit

Permalink
Make enums ordered (#269)
Browse files Browse the repository at this point in the history
Enums are now by default ordered by their definition order, which means
they are now range-able.
  • Loading branch information
joeldrapper authored Dec 20, 2024
1 parent e33af9c commit b1d3770
Show file tree
Hide file tree
Showing 3 changed files with 113 additions and 29 deletions.
2 changes: 1 addition & 1 deletion lib/literal.rb
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,7 @@ module Literal

def self.Enum(type)
Class.new(Literal::Enum) do
prop :value, type, :positional
prop :value, type, :positional, reader: :public
end
end

Expand Down
86 changes: 58 additions & 28 deletions lib/literal/enum.rb
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ class << self
attr_reader :members

def values = @values.keys
def names = @names

def prop(name, type, kind = :keyword, reader: :public, predicate: false, default: nil)
super(name, type, kind, reader:, writer: false, predicate:, default:)
Expand All @@ -17,9 +18,10 @@ def prop(name, type, kind = :keyword, reader: :public, predicate: false, default
def inherited(subclass)
subclass.instance_exec do
@values = {}
@members = Set[]
@members = []
@indexes_definitions = {}
@indexes = {}
@index = {}
@names = {}
end

if RUBY_ENGINE != "truffleruby"
Expand All @@ -32,8 +34,16 @@ def inherited(subclass)
end
end

def position_of(member)
coerce(member).__position__
end

def at_position(n)
@members[n]
end

def index(name, type, unique: true, &block)
@indexes[name] = [type, unique, block || name.to_proc]
@indexes_definitions[name] = [type, unique, block || name.to_proc]
end

def where(**kwargs)
Expand All @@ -43,11 +53,11 @@ def where(**kwargs)

key, value = kwargs.first

types = @indexes.fetch(key)
types = @indexes_definitions.fetch(key)
type = types.first
Literal.check(actual: value, expected: type) { |c| raise NotImplementedError }

@index.fetch(key)[value]
@indexes.fetch(key)[value]
end

def find_by(**kwargs)
Expand All @@ -57,15 +67,15 @@ def find_by(**kwargs)

key, value = kwargs.first

unless @indexes.fetch(key)[1]
unless @indexes_definitions.fetch(key)[1]
raise ArgumentError.new("You can only use `find_by` on unique indexes.")
end

unless (type = @indexes.fetch(key)[0]) === value
unless (type = @indexes_definitions.fetch(key)[0]) === value
raise Literal::TypeError.expected(value, to_be_a: type)
end

@index.fetch(key)[value]&.first
@indexes.fetch(key)[value]&.first
end

def _load(data)
Expand All @@ -77,25 +87,30 @@ def const_added(name)
object = const_get(name)

if self === object
if @values.key?(object.value)
raise ArgumentError.new("The value #{object.value} is already used by #{@values[object.value].name}.")
end
object.instance_variable_set(:@name, name)
@values[object.value] = object
@members << object
# object.instance_variable_set(:@name, name)
@names[object] = name
define_method("#{name.to_s.gsub(/([^A-Z])([A-Z]+)/, '\1_\2').downcase}?") { self == object }
object.freeze
end
end

def new(*args, **kwargs, &block)
raise ArgumentError if frozen?

new_object = super(*args, **kwargs, &nil)

if block
new_object.instance_exec(&block)
if @values.key?(new_object.value)
raise ArgumentError.new("The value #{new_object.value} is already used by #{@values[new_object.value].name}.")
end

@values[new_object.value] = new_object

new_object.instance_variable_set(:@__position__, @members.length)

@members << new_object

new_object.instance_exec(&block) if block

new_object
end

Expand All @@ -106,7 +121,7 @@ def __after_defined__
constants(false).each { |name| const_added(name) }
end

@indexes.each do |name, (type, unique, block)|
@indexes_definitions.each do |name, (type, unique, block)|
index = @members.group_by(&block).freeze

index.each do |key, values|
Expand All @@ -119,7 +134,7 @@ def __after_defined__
end
end

@index[name] = index
@indexes[name] = index
end

@values.freeze
Expand Down Expand Up @@ -179,17 +194,9 @@ def to_h(&)
end
end

def initialize(name, value, &block)
@name = name
@value = value
instance_exec(&block) if block
freeze
end

attr_reader :value

def name
"#{self.class.name}::#{@name}"
klass = self.class
"#{klass.name}::#{klass.names[self]}"
end

alias_method :inspect, :name
Expand All @@ -207,4 +214,27 @@ def deconstruct_keys(keys)
def _dump(level)
Marshal.dump(@value)
end

def <=>(other)
case other
when self.class
@__position__ <=> other.__position__
else
raise ArgumentError.new("Can't compare instances of #{other.class} to instances of #{self.class}")
end
end

def succ
self.class.members[@__position__ + 1]
end

def pred
if @__position__ <= 0
nil
else
self.class.members[@__position__ - 1]
end
end

attr_reader :__position__
end
54 changes: 54 additions & 0 deletions test/enum.test.rb
Original file line number Diff line number Diff line change
Expand Up @@ -79,6 +79,30 @@ class SymbolTypedEnum < Literal::Enum(Symbol)
assert_equal SymbolTypedEnum.coerce(:A), SymbolTypedEnum::B
end

test ".position_of with member" do
assert_equal Color.position_of(Color::Red), 0
assert_equal Color.position_of(Color::Green), 1
assert_equal Color.position_of(Color::Blue), 2
end

test ".position_of with name" do
assert_equal Color.position_of(:Red), 0
assert_equal Color.position_of(:Green), 1
assert_equal Color.position_of(:Blue), 2
end

test ".position_of with value" do
assert_equal Color.position_of(1), 0
assert_equal Color.position_of(2), 1
assert_equal Color.position_of(3), 2
end

test ".at_position" do
assert_equal Color.at_position(0), Color::Red
assert_equal Color.at_position(1), Color::Green
assert_equal Color.at_position(2), Color::Blue
end

test ".to_set" do
assert_equal Color.to_set, Set[
Color::Red,
Expand All @@ -103,6 +127,36 @@ class SymbolTypedEnum < Literal::Enum(Symbol)
]
end

test "#succ" do
assert_equal Color::Red.succ, Color::Green
assert_equal Color::Green.succ, Color::Blue
assert_equal Color::Blue.succ, nil
end

test "#pred" do
assert_equal Color::Red.pred, nil
assert_equal Color::Green.pred, Color::Red
assert_equal Color::Blue.pred, Color::Green
end

test "#<=>" do
assert_equal Color::Red <=> Color::Green, -1
assert_equal Color::Red <=> Color::Red, 0
assert_equal Color::Green <=> Color::Red, 1
end

test "#name" do
assert Color::Red.name.end_with?("Color::Red")
end

test "enums are rangeable" do
range = (Color::Red..Color::Green)

assert Range === range
assert_equal Color::Red, range.begin
assert_equal Color::Green, range.end
end

test "#where" do
assert_equal [Color::Red], Color.where(hex: "#FF0000")
end
Expand Down

0 comments on commit b1d3770

Please sign in to comment.