Skip to content

Commit

Permalink
Better Enums (#93)
Browse files Browse the repository at this point in the history
  • Loading branch information
joeldrapper authored Jun 25, 2024
1 parent 711827c commit f28c773
Show file tree
Hide file tree
Showing 3 changed files with 127 additions and 41 deletions.
2 changes: 1 addition & 1 deletion lib/literal.rb
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@ module Literal

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

Expand Down
132 changes: 97 additions & 35 deletions lib/literal/enum.rb
Original file line number Diff line number Diff line change
@@ -1,15 +1,7 @@
# frozen_string_literal: true

TracePoint.trace(:end) do |tp|
it = tp.self

if Class === it && it < Literal::Enum
it.__after_defined__
end
end

class Literal::Enum
extend Literal::Types
extend Literal::Properties

class << self
include Enumerable
Expand All @@ -18,53 +10,102 @@ class << self

def values = @values.keys

def respond_to_missing?(name, include_private = false)
return super if frozen?
return super unless Symbol === name
return super unless ("A".."Z").include? name[0]

true
def prop(name, type, kind = :keyword, reader: :public, default: nil)
super(name, type, kind, reader:, writer: false, default:)
end

def inherited(subclass)
type = @type

subclass.instance_exec do
@values = {}
@members = Set[]
@type = type
@indexes = {}
@index = {}
end
end

def _load(data)
self[Marshal.load(data)]
def index(name, type, unique: true, &block)
@indexes[name] = [type, unique, block || name.to_proc]
end

def method_missing(name, value, *args, **kwargs, &)
return super if frozen?
return super unless name.is_a?(Symbol)
return super unless name[0] == name[0].upcase
return super if args.length > 0
return super if kwargs.length > 0
def where(**kwargs)
unless kwargs.length == 1
raise ArgumentError.new("You can only specify one index when using `where`.")
end

key, value = kwargs.first

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

@index.fetch(key)[value]
end

def find_by(**kwargs)
unless kwargs.length == 1
raise ArgumentError.new("You can only specify one index when using `where`.")
end

key, value = kwargs.first

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

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

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

raise ArgumentError if @values.key? value
raise ArgumentError if constants.include?(name)
def _load(data)
self[Marshal.load(data)]
end

value = value.dup.freeze unless value.frozen?
def const_added(name)
raise ArgumentError if frozen?
object = const_get(name)

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

Literal.check(value, @type)
def new(*, **, &block)
raise ArgumentError if frozen?
new_object = super(*, **, &nil)

member = new(name, value, &)
const_set name, member
@values[value] = member
@members << member
if block
new_object.instance_exec(&block)
end

define_method("#{name.to_s.gsub(/([^A-Z])([A-Z]+)/, '\1_\2').downcase}?") { self == member }
new_object
end

def __after_defined__
raise ArgumentError if frozen?

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

index.each do |key, values|
unless type === key
raise Literal::TypeError.expected(key, to_be_a: type)
end

if unique && values.size > 1
raise ArgumentError.new("The index #{name} is not unique.")
end
end

@index[name] = index
end

@values.freeze
@members.freeze
freeze
Expand All @@ -84,6 +125,10 @@ def [](value)

alias_method :cast, :[]

def fetch(...)
@values.fetch(...)
end

def to_proc
method(:cast).to_proc
end
Expand All @@ -104,7 +149,24 @@ def name

alias_method :inspect, :name

def deconstruct
[value]
end

def deconstruct_keys(keys)
h = to_h
keys ? h.slice(*keys) : h
end

def _dump(level)
Marshal.dump(@value)
end
end

TracePoint.trace(:end) do |tp|
it = tp.self

if Class === it && it < Literal::Enum
it.__after_defined__
end
end
34 changes: 29 additions & 5 deletions test/enum.test.rb
Original file line number Diff line number Diff line change
Expand Up @@ -3,23 +3,37 @@
extend Literal::Types

class Color < Literal::Enum(Integer)
Red(1)
Green(2)
Blue(3)
prop :hex, String

index :hex, String

index :lower_hex, String, unique: false do |color|
color.hex.downcase
end

Red = new(1, hex: "#FF0000")
Green = new(2, hex: "#00FF00")
Blue = new(3, hex: "#0000FF")
end

class Switch < Literal::Enum(_Boolean)
On(true) do
On = new(true) do
def toggle = Off
end

Off(false) do
Off = new(false) do
def toggle = On
end
end

test do
expect(Color::Red.name) =~ /Color::Red\z/
expect(Color.where(hex: "#FF0000")) == [Color::Red]
expect(Color.find_by(hex: "#FF0000")) == Color::Red
expect { Color.find_by(lower_hex: "#ff0000") }.to_raise(ArgumentError)
expect(Color.where(lower_hex: "#ff0000")) == [Color::Red]
expect(Color::Red.value) == 1
expect(Color::Red.hex) == "#FF0000"
expect(Color::Red.red?) == true
expect(Color::Red.green?) == false
expect(Color).to_be(:frozen?)
Expand All @@ -33,3 +47,13 @@ def toggle = On
expect(Switch::Off.toggle) == Switch::On
expect(Switch::On.toggle) == Switch::Off
end

test "pattern matching" do
Color::Red => Color
Color::Red => Color[1]
Color::Red => Color[hex: "#FF0000"]

Color::Red => Color
Color::Red => Color[1]
Color::Red => Color::Red[hex: "#FF0000"]
end

0 comments on commit f28c773

Please sign in to comment.