Skip to content

Commit 97e84df

Browse files
committed
Ruby 3.2 struct compatibility
1 parent 2fdd257 commit 97e84df

File tree

3 files changed

+340
-39
lines changed

3 files changed

+340
-39
lines changed

Gemfile.lock

+16-15
Original file line numberDiff line numberDiff line change
@@ -7,30 +7,31 @@ PATH
77
GEM
88
remote: https://rubygems.org/
99
specs:
10-
diff-lcs (1.4.4)
11-
rake (13.0.3)
12-
rbs (1.5.1)
13-
rspec (3.10.0)
14-
rspec-core (~> 3.10.0)
15-
rspec-expectations (~> 3.10.0)
16-
rspec-mocks (~> 3.10.0)
17-
rspec-core (3.10.1)
18-
rspec-support (~> 3.10.0)
19-
rspec-expectations (3.10.1)
10+
diff-lcs (1.5.0)
11+
rake (13.0.6)
12+
rbs (1.8.1)
13+
rspec (3.11.0)
14+
rspec-core (~> 3.11.0)
15+
rspec-expectations (~> 3.11.0)
16+
rspec-mocks (~> 3.11.0)
17+
rspec-core (3.11.0)
18+
rspec-support (~> 3.11.0)
19+
rspec-expectations (3.11.1)
2020
diff-lcs (>= 1.2.0, < 2.0)
21-
rspec-support (~> 3.10.0)
22-
rspec-mocks (3.10.2)
21+
rspec-support (~> 3.11.0)
22+
rspec-mocks (3.11.1)
2323
diff-lcs (>= 1.2.0, < 2.0)
24-
rspec-support (~> 3.10.0)
25-
rspec-support (3.10.2)
24+
rspec-support (~> 3.11.0)
25+
rspec-support (3.11.1)
2626

2727
PLATFORMS
2828
ruby
29+
x86_64-linux
2930

3031
DEPENDENCIES
3132
rake (~> 13.0)
3233
rspec (~> 3.0)
3334
typed_struct!
3435

3536
BUNDLED WITH
36-
2.1.4
37+
2.4.0.dev

lib/typed_struct.rb

+50-7
Original file line numberDiff line numberDiff line change
@@ -19,13 +19,17 @@ class TypedStruct < Struct
1919

2020
class << self
2121
def new(opts = Options.new, **properties)
22+
if const_defined?("RSpec") && RUBY_VERSION < "3.2" && opts[:keyword_init].nil?
23+
opts[:keyword_init] = false
24+
end
25+
2226
properties.each_key do |prop|
2327
if method_defined?(prop)
24-
$stdout.puts OVERRIDING_NATIVE_METHOD_MSG % [prop.inspect, caller(3).first]
28+
warn OVERRIDING_NATIVE_METHOD_MSG % [prop.inspect, caller(3).first]
2529
end
2630
end
2731

28-
super(*properties.keys, keyword_init: true).tap do |klass|
32+
super(opts[:class_name], *properties.keys, keyword_init: opts[:keyword_init]).tap do |klass|
2933
klass.class.instance_eval do
3034
include TypeChecking
3135
attr_reader :options
@@ -35,9 +39,25 @@ def new(opts = Options.new, **properties)
3539
@options = { types: properties, options: opts }
3640

3741
define_method :[]= do |key, val|
42+
if key.is_a?(Integer)
43+
key = if key.negative?
44+
offset = self.members.size + key
45+
if offset.negative?
46+
raise IndexError, "offset #{key} too small for struct(size:#{self.members.size})"
47+
end
48+
self.members[offset]
49+
elsif key >= self.members.size
50+
raise IndexError, "offset #{key} too large for struct(size:#{self.members.size})"
51+
else
52+
self.members[key]
53+
end
54+
end
55+
unless properties.key?(key)
56+
raise NameError, "no member '#{key}' in struct"
57+
end
3858
prop = properties[key]
3959
unless val_is_type? val, prop
40-
raise "Unexpected type #{val.class} for #{key.inspect} (expected #{prop})"
60+
raise TypeError, "unexpected type #{val.class} for #{key.inspect} (expected #{prop})"
4161
end
4262

4363
super key, val
@@ -53,18 +73,41 @@ def new(opts = Options.new, **properties)
5373
end
5474
end
5575

56-
def initialize(**attrs)
76+
def initialize(*positional_attrs, **attrs)
5777
opts = self.__class__.options
78+
if opts[:options][:keyword_init] == true && !positional_attrs.empty?
79+
raise ArgumentError, "wrong number of arguments (given #{positional_attrs.size}, expected 0)"
80+
elsif (opts[:options][:keyword_init] == false && !attrs.empty?) ||
81+
(opts[:options][:keyword_init] != true && !positional_attrs.empty?)
82+
positional_attrs << attrs unless attrs.empty?
83+
attrs = positional_attrs.zip(self.members).to_h(&:reverse)
84+
end
85+
86+
if !positional_attrs.empty? && attrs.size > self.members.size
87+
raise ArgumentError, "struct size differs"
88+
elsif !(attrs.keys - self.members).empty?
89+
raise ArgumentError, "unknown keywords: #{(attrs.keys - self.members).join(', ')}"
90+
end
91+
5892
vals = opts[:types].to_h do |prop, expected_type|
5993
value = attrs.fetch(prop, opts[:options][:default])
6094
unless val_is_type? value, expected_type
61-
raise "Unexpected type #{value.class} for #{prop.inspect} (expected #{expected_type})"
95+
raise TypeError, "unexpected type #{value.class} for #{prop.inspect} (expected #{expected_type})"
6296
end
6397
[prop, value]
6498
end
6599

66-
super **vals
100+
if opts[:options][:keyword_init]
101+
super **vals
102+
else
103+
super *vals.values
104+
end
67105
end
68106

69-
Options = TypedStruct.new({ default: nil }, default: Rbs("untyped"))
107+
Options = TypedStruct.new(
108+
{ default: nil, keyword_init: true },
109+
default: Rbs("untyped"),
110+
keyword_init: Rbs("bool?"),
111+
class_name: Rbs("String? | Symbol?")
112+
)
70113
end

0 commit comments

Comments
 (0)