Skip to content

Commit

Permalink
Add Data (3.2)
Browse files Browse the repository at this point in the history
  • Loading branch information
marcandre committed Mar 7, 2023
1 parent c65fb3e commit 05cc719
Show file tree
Hide file tree
Showing 2 changed files with 153 additions and 0 deletions.
4 changes: 4 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -115,6 +115,10 @@ itself, JRuby and Rubinius.

- `attached_object`

#### Data

- Complete class

#### Enumerator

- `Enumerator.product` and `Enumerator::Product`
Expand Down
149 changes: 149 additions & 0 deletions lib/backports/3.2.0/data.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,149 @@
original_verbosity = $VERBOSE
$VERBOSE = nil
if defined?(::Data) && !::Data.respond_to?(:define)
Object.send(:remove_const, :Data)
end
class ::Data
end

module Backports
Data = ::Data
end
$VERBOSE = original_verbosity

unless ::Backports::Data.respond_to?(:define)
require "backports/2.7.0/symbol/end_with"
require "backports/2.5.0/module/define_method"

class ::Backports::Data
def deconstruct
@__members__.values
end

def deconstruct_keys(keys_or_nil)
return @__members__ unless keys_or_nil

raise TypeError, "Expected symbols" unless keys_or_nil.is_a?(Array) && keys_or_nil.all? {|s| s.is_a?(Symbol)}
@__members__.slice(*keys_or_nil)
end

def self.define(*members, &block)
members.each do |m|
raise TypeError, "#{m} is not a Symbol" unless m.is_a?(Symbol) || m.is_a?(String)
raise ArgumentError, "invalid data member: #{m}" if m.end_with?("=")
end
members = members.map(&:to_sym)
raise ArgumentError, "duplicate members" if members.uniq!

klass = instance_eval <<-"end_define", __FILE__, __LINE__ + 1
Class.new(::Backports::Data) do # Class.new(::Data) do
def self.members # def self.members
#{members.inspect} # [:a_member, :another_member]
end # end
end # end
end_define

members.each do |m|
klass.define_method(m) { @__members__[m]}
end

class << klass
def new(*values, **named_values)
if named_values.empty?
case values.size <=> members.size
when -1
missing = members[values.size..-1].map(:inspect).join(", ")
raise ArgumentError, "Missing keywords: #{missing}"

This comment has been minimized.

Copy link
@zverok

zverok Mar 25, 2023

Contributor

BTW, this doesn't correspond to native Data behaviour. In case there is not enough positional members, it will just pass those present, which allows initialize to handle defaults.

Point = Data.define(:x, :y, :z) do
  def initialize(x:, y:, z: 0) = super
end

Point.new(1, 2) #=> #<Point x=1 y=2 z=0>

See also https://zverok.space/blog/2023-01-03-data-initialize.html

This comment has been minimized.

Copy link
@marcandre

marcandre Mar 27, 2023

Author Owner

Right, I see. Thanks, I opened an issue. So I think there's a also a missing test in MRI about this as I used those tests against my implementation...

This comment has been minimized.

Copy link
@zverok

zverok Mar 27, 2023

Contributor

Yeah, seems here only the test about missing keyword args is present, I missed to add positional one 😢

when +1
raise ArgumentError, "wrong number of arguments (given #{values.size}, expected 0..#{members.size})"
when 0
super(**members.zip(values).to_h)
end
else
unless values.empty?
raise ArgumentError, "wrong number of arguments (given #{values.size}, expected 0)"
end
super(**named_values)
end
end
undef :define
end

klass.class_eval(&block) if block

klass
end

def eql?(other)
return false unless other.instance_of?(self.class)

@__members__.eql?(other.to_h)
end

def ==(other)
return false unless other.instance_of?(self.class)

@__members__ == other.to_h
end

def hash
@__members__.hash
end

def initialize(**named_values)
given = named_values.keys
missing = members - given
unless missing.empty?
missing = missing.map(&:inspect).join(", ")
raise ArgumentError, "missing keywords: #{missing}"
end
if members.size < given.size
extra = (given - members).map(&:inspect).join(", ")
raise ArgumentError, "unknown keywords: #{extra}"
end
@__members__ = named_values.freeze
freeze
end

# Why is `initialize_copy` specialized in MRI and not just `initialize_dup`?
# Let's follow the pattern anyways
def initialize_copy(other)
@__members__ = other.to_h
freeze
end

def inspect
data = @__members__.map {|k, v| "#{k}=#{v.inspect}"}.join(", ")
space = data != "" && self.class.name ? " " : ""
"#<data #{self.class.name}#{space}#{data}>"
end

def marshal_dump
@__members__
end

def marshal_load(members)
@__members__ = members
freeze
end

# class method defined in `define`
def members
self.class.members
end

class << self
private :new
end

def to_h(&block)
@__members__.to_h(&block)
end

def with(**update)
return self if update.empty?

self.class.new(**@__members__.merge(update))
end
end
end

0 comments on commit 05cc719

Please sign in to comment.