ruby-next-core 0.15.3 → 1.0.0.rc.1

Sign up to get free protection for your applications and to get access to all the features.
Files changed (58) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +28 -0
  3. data/README.md +118 -48
  4. data/bin/mspec +11 -0
  5. data/lib/.rbnext/2.1/ruby-next/commands/nextify.rb +295 -0
  6. data/lib/.rbnext/2.1/ruby-next/core.rb +10 -2
  7. data/lib/.rbnext/2.1/ruby-next/language.rb +54 -10
  8. data/lib/.rbnext/2.3/ruby-next/commands/nextify.rb +82 -2
  9. data/lib/.rbnext/2.3/ruby-next/config.rb +79 -0
  10. data/lib/.rbnext/2.3/ruby-next/core/data.rb +159 -0
  11. data/lib/.rbnext/2.3/ruby-next/language/rewriters/2.7/pattern_matching.rb +2 -2
  12. data/lib/.rbnext/2.3/ruby-next/language/rewriters/base.rb +6 -32
  13. data/lib/.rbnext/2.3/ruby-next/utils.rb +3 -22
  14. data/lib/.rbnext/2.6/ruby-next/core/data.rb +159 -0
  15. data/lib/.rbnext/2.7/ruby-next/core/data.rb +159 -0
  16. data/lib/.rbnext/2.7/ruby-next/core.rb +10 -2
  17. data/lib/.rbnext/2.7/ruby-next/language/paco_parsers/string_literals.rb +109 -0
  18. data/lib/.rbnext/2.7/ruby-next/language/rewriters/2.7/pattern_matching.rb +2 -2
  19. data/lib/.rbnext/2.7/ruby-next/language/rewriters/text.rb +132 -0
  20. data/lib/.rbnext/3.2/ruby-next/commands/base.rb +55 -0
  21. data/lib/.rbnext/3.2/ruby-next/language/rewriters/2.7/pattern_matching.rb +1095 -0
  22. data/lib/.rbnext/3.2/ruby-next/rubocop.rb +210 -0
  23. data/lib/ruby-next/commands/nextify.rb +84 -2
  24. data/lib/ruby-next/config.rb +27 -0
  25. data/lib/ruby-next/core/data.rb +159 -0
  26. data/lib/ruby-next/core/matchdata/deconstruct.rb +9 -0
  27. data/lib/ruby-next/core/matchdata/deconstruct_keys.rb +20 -0
  28. data/lib/ruby-next/core/matchdata/named_captures.rb +11 -0
  29. data/lib/ruby-next/core/refinement/import.rb +44 -36
  30. data/lib/ruby-next/core/time/deconstruct_keys.rb +30 -0
  31. data/lib/ruby-next/core.rb +10 -2
  32. data/lib/ruby-next/irb.rb +2 -2
  33. data/lib/ruby-next/language/bootsnap.rb +2 -25
  34. data/lib/ruby-next/language/paco_parser.rb +7 -0
  35. data/lib/ruby-next/language/paco_parsers/base.rb +47 -0
  36. data/lib/ruby-next/language/paco_parsers/comments.rb +26 -0
  37. data/lib/ruby-next/language/paco_parsers/string_literals.rb +109 -0
  38. data/lib/ruby-next/language/parser.rb +24 -2
  39. data/lib/ruby-next/language/rewriters/3.0/args_forward_leading.rb +2 -2
  40. data/lib/ruby-next/language/rewriters/3.1/oneline_pattern_parensless.rb +1 -1
  41. data/lib/ruby-next/language/rewriters/3.2/anonymous_restargs.rb +104 -0
  42. data/lib/ruby-next/language/rewriters/abstract.rb +57 -0
  43. data/lib/ruby-next/language/rewriters/base.rb +6 -32
  44. data/lib/ruby-next/language/rewriters/edge/it_param.rb +58 -0
  45. data/lib/ruby-next/language/rewriters/edge.rb +12 -0
  46. data/lib/ruby-next/language/rewriters/proposed/bind_vars_pattern.rb +3 -0
  47. data/lib/ruby-next/language/rewriters/proposed/method_reference.rb +9 -20
  48. data/lib/ruby-next/language/rewriters/text.rb +132 -0
  49. data/lib/ruby-next/language/runtime.rb +9 -86
  50. data/lib/ruby-next/language/setup.rb +5 -2
  51. data/lib/ruby-next/language/unparser.rb +5 -0
  52. data/lib/ruby-next/language.rb +54 -10
  53. data/lib/ruby-next/pry.rb +1 -1
  54. data/lib/ruby-next/rubocop.rb +2 -0
  55. data/lib/ruby-next/utils.rb +3 -22
  56. data/lib/ruby-next/version.rb +1 -1
  57. data/lib/uby-next.rb +2 -2
  58. metadata +65 -12
@@ -0,0 +1,79 @@
1
+ # frozen_string_literal: true
2
+
3
+ module RubyNext
4
+ # Mininum Ruby version supported by RubyNext
5
+ MIN_SUPPORTED_VERSION = Gem::Version.new("2.2.0")
6
+
7
+ # Where to store transpiled files (relative from the project LOAD_PATH, usually `lib/`)
8
+ RUBY_NEXT_DIR = ".rbnext"
9
+
10
+ # Defines last minor version for every major version
11
+ LAST_MINOR_VERSIONS = {
12
+ 2 => 8, # 2.8 is required for backward compatibility: some gems already uses it
13
+ 3 => 1
14
+ }.freeze
15
+
16
+ LATEST_VERSION = [3, 1].freeze
17
+
18
+ # A virtual version number used for proposed features
19
+ NEXT_VERSION = "1995.next.0"
20
+
21
+ class << self
22
+ # TruffleRuby claims its RUBY_VERSION to be X.Y while not supporting all the features
23
+ # Currently (23.0.1), it still doesn't support pattern matching, although claims to be "like 3.1".
24
+ # So, we fallback to 2.6.5 (since we cannot use 2.7).
25
+ if defined?(TruffleRuby)
26
+ def current_ruby_version
27
+ "2.6.5"
28
+ end
29
+ else
30
+ def current_ruby_version
31
+ ::RUBY_VERSION
32
+ end
33
+ end
34
+
35
+ # Returns true if we want to use edge syntax
36
+ def edge_syntax?
37
+ %w[y true 1].include?(ENV["RUBY_NEXT_EDGE"])
38
+ end
39
+
40
+ def proposed_syntax?
41
+ %w[y true 1].include?(ENV["RUBY_NEXT_PROPOSED"])
42
+ end
43
+
44
+ def next_ruby_version(version = current_ruby_version)
45
+ return if version == Gem::Version.new(NEXT_VERSION)
46
+
47
+ major, minor = Gem::Version.new(version).segments.map(&:to_i)
48
+
49
+ return Gem::Version.new(NEXT_VERSION) if major >= LATEST_VERSION.first && minor >= LATEST_VERSION.last
50
+
51
+ nxt =
52
+ if LAST_MINOR_VERSIONS[major] == minor
53
+ "#{major + 1}.0.0"
54
+ else
55
+ "#{major}.#{minor + 1}.0"
56
+ end
57
+
58
+ Gem::Version.new(nxt)
59
+ end
60
+
61
+ # Load transpile settings from the RC file (nextify command flags)
62
+ def load_from_rc(path = ".rbnextrc")
63
+ return unless File.exist?(path)
64
+
65
+ require "yaml"
66
+
67
+ args = ((((__safe_lvar__ = ((((__safe_lvar__ = ((((__safe_lvar__ = YAML.load_file(path)) || true) && (!__safe_lvar__.nil? || nil)) && __safe_lvar__.fetch("nextify", ""))) || true) && (!__safe_lvar__.nil? || nil)) && __safe_lvar__.lines)) || true) && (!__safe_lvar__.nil? || nil)) && __safe_lvar__.flat_map { |line|
68
+ line.chomp.split(/\s+/)
69
+ })
70
+
71
+ ENV["RUBY_NEXT_EDGE"] ||= "true" if args.delete("--edge")
72
+ ENV["RUBY_NEXT_PROPOSED"] ||= "true" if args.delete("--proposed")
73
+ ENV["RUBY_NEXT_TRANSPILE_MODE"] ||= "rewrite" if args.delete("--transpile-mode=rewrite")
74
+ ENV["RUBY_NEXT_TRANSPILE_MODE"] ||= "ast" if args.delete("--transpile-mode=ast")
75
+ end
76
+ end
77
+
78
+ load_from_rc
79
+ end
@@ -0,0 +1,159 @@
1
+ # frozen_string_literal: true
2
+
3
+ # The code below originates from https://github.com/saturnflyer/polyfill-data
4
+
5
+ if !Object.const_defined?(:Data) || !Data.respond_to?(:define)
6
+
7
+ # Drop legacy Data class
8
+ begin
9
+ Object.send(:remove_const, :Data)
10
+ rescue
11
+ nil
12
+ end
13
+
14
+ class Data < Object
15
+ using RubyNext
16
+
17
+ class << self
18
+ undef_method :new
19
+ attr_reader :members
20
+ end
21
+
22
+ def self.define(*args, &block)
23
+ raise ArgumentError if args.any?(/=/)
24
+ if block
25
+ mod = Module.new
26
+ mod.define_singleton_method(:_) do |klass|
27
+ klass.class_eval(&block)
28
+ end
29
+ arity_converter = mod.method(:_)
30
+ end
31
+ klass = ::Class.new(self)
32
+
33
+ klass.instance_variable_set(:@members, args.map(&:to_sym).freeze)
34
+
35
+ klass.define_singleton_method(:new) do |*new_args, **new_kwargs, &block|
36
+ init_kwargs = if new_args.any?
37
+ raise ArgumentError, "unknown arguments #{new_args[members.size..-1].join(", ")}" if new_args.size > members.size
38
+ members.take(new_args.size).zip(new_args).to_h
39
+ else
40
+ new_kwargs
41
+ end
42
+
43
+ allocate.tap do |instance|
44
+ instance.send(:initialize, **init_kwargs, &block)
45
+ end.freeze
46
+ end
47
+
48
+ class << klass
49
+ alias_method :[], :new
50
+ undef_method :define
51
+ end
52
+
53
+ args.each do |arg|
54
+ if klass.method_defined?(arg)
55
+ raise ArgumentError, "duplicate member #{arg}"
56
+ end
57
+ klass.define_method(arg) do
58
+ @attributes[arg]
59
+ end
60
+ end
61
+
62
+ if arity_converter
63
+ klass.class_eval(&arity_converter)
64
+ end
65
+
66
+ klass
67
+ end
68
+
69
+ def members
70
+ self.class.members
71
+ end
72
+
73
+ def initialize(**kwargs)
74
+ kwargs_size = kwargs.size
75
+ members_size = members.size
76
+
77
+ if kwargs_size > members_size
78
+ extras = kwargs.except(*members).keys
79
+
80
+ if extras.size > 1
81
+ raise ArgumentError, "unknown keywords: #{extras.map { |_1| ":#{_1}" }.join(", ")}"
82
+ else
83
+ raise ArgumentError, "unknown keyword: :#{extras.first}"
84
+ end
85
+ elsif kwargs_size < members_size
86
+ missing = members.select { |k| !kwargs.include?(k) }
87
+
88
+ if missing.size > 1
89
+ raise ArgumentError, "missing keywords: #{missing.map { |_1| ":#{_1}" }.join(", ")}"
90
+ else
91
+ raise ArgumentError, "missing keyword: :#{missing.first}"
92
+ end
93
+ end
94
+
95
+ @attributes = members.map { |m| [m, kwargs[m]] }.to_h
96
+ end
97
+
98
+ def deconstruct
99
+ @attributes.values
100
+ end
101
+
102
+ def deconstruct_keys(array)
103
+ raise TypeError unless array.is_a?(Array) || array.nil?
104
+ return @attributes if ((((__safe_lvar__ = array) || true) && (!__safe_lvar__.nil? || nil)) && __safe_lvar__.first).nil?
105
+
106
+ @attributes.slice(*array)
107
+ end
108
+
109
+ def to_h(&block)
110
+ @attributes.to_h(&block)
111
+ end
112
+
113
+ def hash
114
+ to_h.hash
115
+ end
116
+
117
+ def eql?(other)
118
+ self.class == other.class && hash == other.hash
119
+ end
120
+
121
+ def ==(other)
122
+ self.class == other.class && to_h == other.to_h
123
+ end
124
+
125
+ def inspect
126
+ attribute_markers = @attributes.map do |key, value|
127
+ insect_key = key.to_s.start_with?("@") ? ":#{key}" : key
128
+ "#{insect_key}=#{value}"
129
+ end.join(", ")
130
+
131
+ display = ["data", self.class.name, attribute_markers].compact.join(" ")
132
+
133
+ "#<#{display}>"
134
+ end
135
+ alias_method :to_s, :inspect
136
+
137
+ def with(**kwargs)
138
+ return self if kwargs.empty?
139
+
140
+ self.class.new(**@attributes.merge(kwargs))
141
+ end
142
+
143
+ private
144
+
145
+ def marshal_dump
146
+ @attributes
147
+ end
148
+
149
+ def marshal_load(attributes)
150
+ @attributes = attributes
151
+ freeze
152
+ end
153
+
154
+ def initialize_copy(source)
155
+ super.freeze
156
+ end
157
+ end
158
+
159
+ end
@@ -185,7 +185,7 @@ module RubyNext
185
185
  # rubocop:disable Style/MissingRespondToMissing
186
186
  class Noop < Base
187
187
  # Return node itself, no memoization
188
- def method_missing(mid, node, *)
188
+ def method_missing(mid, node, *__rest__)
189
189
  node
190
190
  end
191
191
  end
@@ -1035,7 +1035,7 @@ module RubyNext
1035
1035
  s(:send, node, :respond_to?, mid.to_ast_node)
1036
1036
  end
1037
1037
 
1038
- def respond_to_missing?(mid, *)
1038
+ def respond_to_missing?(mid, *__rest__)
1039
1039
  return true if mid.to_s.match?(/_(clause|array_element)/)
1040
1040
  super
1041
1041
  end
@@ -12,7 +12,7 @@ module RubyNext
12
12
 
13
13
  MSG
14
14
 
15
- class Base < ::Parser::TreeRewriter
15
+ class Base < Abstract
16
16
  class LocalsTracker
17
17
  using(Module.new do
18
18
  refine ::Parser::AST::Node do
@@ -64,39 +64,15 @@ module RubyNext
64
64
  end
65
65
  end
66
66
 
67
- class << self
68
- # Returns true if the syntax is not supported
69
- # by the current Ruby (performs syntax check, not version check)
70
- def unsupported_syntax?
71
- save_verbose, $VERBOSE = $VERBOSE, nil
72
- eval_mid = Kernel.respond_to?(:eval_without_ruby_next) ? :eval_without_ruby_next : :eval
73
- Kernel.send eval_mid, self::SYNTAX_PROBE, nil, __FILE__, __LINE__
74
- false
75
- rescue SyntaxError, StandardError
76
- true
77
- ensure
78
- $VERBOSE = save_verbose
79
- end
80
-
81
- # Returns true if the syntax is supported
82
- # by the specified version
83
- def unsupported_version?(version)
84
- self::MIN_SUPPORTED_VERSION > version
85
- end
86
-
87
- private
67
+ attr_reader :locals
88
68
 
89
- def transform(source)
90
- Language.transform(source, rewriters: [self], using: false)
91
- end
69
+ def self.ast?
70
+ true
92
71
  end
93
72
 
94
- attr_reader :locals
95
-
96
- def initialize(context)
97
- @context = context
73
+ def initialize(*args)
98
74
  @locals = LocalsTracker.new
99
- super()
75
+ super
100
76
  end
101
77
 
102
78
  def s(type, *children)
@@ -144,8 +120,6 @@ module RubyNext
144
120
 
145
121
  Unparser.unparse(ast).chomp
146
122
  end
147
-
148
- attr_reader :context
149
123
  end
150
124
  end
151
125
  end
@@ -4,28 +4,6 @@ module RubyNext
4
4
  module Utils
5
5
  module_function
6
6
 
7
- if $LOAD_PATH.respond_to?(:resolve_feature_path)
8
- def resolve_feature_path(feature)
9
- ((((__safe_lvar__ = $LOAD_PATH.resolve_feature_path(feature)) || true) && (!__safe_lvar__.nil? || nil)) && __safe_lvar__.last)
10
- rescue LoadError
11
- end
12
- else
13
- def resolve_feature_path(path)
14
- if File.file?(relative = File.expand_path(path))
15
- path = relative
16
- end
17
-
18
- path = "#{path}.rb" if File.extname(path).empty?
19
-
20
- return path if Pathname.new(path).absolute?
21
-
22
- $LOAD_PATH.find do |lp|
23
- lpath = File.join(lp, path)
24
- return File.realpath(lpath) if File.file?(lpath)
25
- end
26
- end
27
- end
28
-
29
7
  def source_with_lines(source, path)
30
8
  source.lines.map.with_index do |line, i|
31
9
  "#{(i + 1).to_s.rjust(4)}: #{line}"
@@ -36,6 +14,7 @@ module RubyNext
36
14
 
37
15
  # Returns true if modules refinement is supported in current version
38
16
  def refine_modules?
17
+ save_verbose, $VERBOSE = $VERBOSE, nil
39
18
  @refine_modules ||=
40
19
  begin
41
20
  # Make sure that including modules within refinements works
@@ -59,6 +38,8 @@ module RubyNext
59
38
  true
60
39
  rescue TypeError, NoMethodError
61
40
  false
41
+ ensure
42
+ $VERBOSE = save_verbose
62
43
  end
63
44
  end
64
45
  end
@@ -0,0 +1,159 @@
1
+ # frozen_string_literal: true
2
+
3
+ # The code below originates from https://github.com/saturnflyer/polyfill-data
4
+
5
+ if !Object.const_defined?(:Data) || !Data.respond_to?(:define)
6
+
7
+ # Drop legacy Data class
8
+ begin
9
+ Object.send(:remove_const, :Data)
10
+ rescue
11
+ nil
12
+ end
13
+
14
+ class Data < Object
15
+ using RubyNext
16
+
17
+ class << self
18
+ undef_method :new
19
+ attr_reader :members
20
+ end
21
+
22
+ def self.define(*args, &block)
23
+ raise ArgumentError if args.any?(/=/)
24
+ if block
25
+ mod = Module.new
26
+ mod.define_singleton_method(:_) do |klass|
27
+ klass.class_eval(&block)
28
+ end
29
+ arity_converter = mod.method(:_)
30
+ end
31
+ klass = ::Class.new(self)
32
+
33
+ klass.instance_variable_set(:@members, args.map(&:to_sym).freeze)
34
+
35
+ klass.define_singleton_method(:new) do |*new_args, **new_kwargs, &block|
36
+ init_kwargs = if new_args.any?
37
+ raise ArgumentError, "unknown arguments #{new_args[members.size..-1].join(", ")}" if new_args.size > members.size
38
+ members.take(new_args.size).zip(new_args).to_h
39
+ else
40
+ new_kwargs
41
+ end
42
+
43
+ allocate.tap do |instance|
44
+ instance.send(:initialize, **init_kwargs, &block)
45
+ end.freeze
46
+ end
47
+
48
+ class << klass
49
+ alias_method :[], :new
50
+ undef_method :define
51
+ end
52
+
53
+ args.each do |arg|
54
+ if klass.method_defined?(arg)
55
+ raise ArgumentError, "duplicate member #{arg}"
56
+ end
57
+ klass.define_method(arg) do
58
+ @attributes[arg]
59
+ end
60
+ end
61
+
62
+ if arity_converter
63
+ klass.class_eval(&arity_converter)
64
+ end
65
+
66
+ klass
67
+ end
68
+
69
+ def members
70
+ self.class.members
71
+ end
72
+
73
+ def initialize(**kwargs)
74
+ kwargs_size = kwargs.size
75
+ members_size = members.size
76
+
77
+ if kwargs_size > members_size
78
+ extras = kwargs.except(*members).keys
79
+
80
+ if extras.size > 1
81
+ raise ArgumentError, "unknown keywords: #{extras.map { |_1| ":#{_1}" }.join(", ")}"
82
+ else
83
+ raise ArgumentError, "unknown keyword: :#{extras.first}"
84
+ end
85
+ elsif kwargs_size < members_size
86
+ missing = members.select { |k| !kwargs.include?(k) }
87
+
88
+ if missing.size > 1
89
+ raise ArgumentError, "missing keywords: #{missing.map { |_1| ":#{_1}" }.join(", ")}"
90
+ else
91
+ raise ArgumentError, "missing keyword: :#{missing.first}"
92
+ end
93
+ end
94
+
95
+ @attributes = members.map { |m| [m, kwargs[m]] }.to_h
96
+ end
97
+
98
+ def deconstruct
99
+ @attributes.values
100
+ end
101
+
102
+ def deconstruct_keys(array)
103
+ raise TypeError unless array.is_a?(Array) || array.nil?
104
+ return @attributes if array&.first.nil?
105
+
106
+ @attributes.slice(*array)
107
+ end
108
+
109
+ def to_h(&block)
110
+ @attributes.to_h(&block)
111
+ end
112
+
113
+ def hash
114
+ to_h.hash
115
+ end
116
+
117
+ def eql?(other)
118
+ self.class == other.class && hash == other.hash
119
+ end
120
+
121
+ def ==(other)
122
+ self.class == other.class && to_h == other.to_h
123
+ end
124
+
125
+ def inspect
126
+ attribute_markers = @attributes.map do |key, value|
127
+ insect_key = key.to_s.start_with?("@") ? ":#{key}" : key
128
+ "#{insect_key}=#{value}"
129
+ end.join(", ")
130
+
131
+ display = ["data", self.class.name, attribute_markers].compact.join(" ")
132
+
133
+ "#<#{display}>"
134
+ end
135
+ alias_method :to_s, :inspect
136
+
137
+ def with(**kwargs)
138
+ return self if kwargs.empty?
139
+
140
+ self.class.new(**@attributes.merge(kwargs))
141
+ end
142
+
143
+ private
144
+
145
+ def marshal_dump
146
+ @attributes
147
+ end
148
+
149
+ def marshal_load(attributes)
150
+ @attributes = attributes
151
+ freeze
152
+ end
153
+
154
+ def initialize_copy(source)
155
+ super.freeze
156
+ end
157
+ end
158
+
159
+ end
@@ -0,0 +1,159 @@
1
+ # frozen_string_literal: true
2
+
3
+ # The code below originates from https://github.com/saturnflyer/polyfill-data
4
+
5
+ if !Object.const_defined?(:Data) || !Data.respond_to?(:define)
6
+
7
+ # Drop legacy Data class
8
+ begin
9
+ Object.send(:remove_const, :Data)
10
+ rescue
11
+ nil
12
+ end
13
+
14
+ class Data < Object
15
+ using RubyNext
16
+
17
+ class << self
18
+ undef_method :new
19
+ attr_reader :members
20
+ end
21
+
22
+ def self.define(*args, &block)
23
+ raise ArgumentError if args.any?(/=/)
24
+ if block
25
+ mod = Module.new
26
+ mod.define_singleton_method(:_) do |klass|
27
+ klass.class_eval(&block)
28
+ end
29
+ arity_converter = mod.method(:_)
30
+ end
31
+ klass = ::Class.new(self)
32
+
33
+ klass.instance_variable_set(:@members, args.map(&:to_sym).freeze)
34
+
35
+ klass.define_singleton_method(:new) do |*new_args, **new_kwargs, &block|
36
+ init_kwargs = if new_args.any?
37
+ raise ArgumentError, "unknown arguments #{new_args[members.size..].join(", ")}" if new_args.size > members.size
38
+ members.take(new_args.size).zip(new_args).to_h
39
+ else
40
+ new_kwargs
41
+ end
42
+
43
+ allocate.tap do |instance|
44
+ instance.send(:initialize, **init_kwargs, &block)
45
+ end.freeze
46
+ end
47
+
48
+ class << klass
49
+ alias_method :[], :new
50
+ undef_method :define
51
+ end
52
+
53
+ args.each do |arg|
54
+ if klass.method_defined?(arg)
55
+ raise ArgumentError, "duplicate member #{arg}"
56
+ end
57
+ klass.define_method(arg) do
58
+ @attributes[arg]
59
+ end
60
+ end
61
+
62
+ if arity_converter
63
+ klass.class_eval(&arity_converter)
64
+ end
65
+
66
+ klass
67
+ end
68
+
69
+ def members
70
+ self.class.members
71
+ end
72
+
73
+ def initialize(**kwargs)
74
+ kwargs_size = kwargs.size
75
+ members_size = members.size
76
+
77
+ if kwargs_size > members_size
78
+ extras = kwargs.except(*members).keys
79
+
80
+ if extras.size > 1
81
+ raise ArgumentError, "unknown keywords: #{extras.map { |_1| ":#{_1}" }.join(", ")}"
82
+ else
83
+ raise ArgumentError, "unknown keyword: :#{extras.first}"
84
+ end
85
+ elsif kwargs_size < members_size
86
+ missing = members.select { |k| !kwargs.include?(k) }
87
+
88
+ if missing.size > 1
89
+ raise ArgumentError, "missing keywords: #{missing.map { |_1| ":#{_1}" }.join(", ")}"
90
+ else
91
+ raise ArgumentError, "missing keyword: :#{missing.first}"
92
+ end
93
+ end
94
+
95
+ @attributes = members.map { |m| [m, kwargs[m]] }.to_h
96
+ end
97
+
98
+ def deconstruct
99
+ @attributes.values
100
+ end
101
+
102
+ def deconstruct_keys(array)
103
+ raise TypeError unless array.is_a?(Array) || array.nil?
104
+ return @attributes if array&.first.nil?
105
+
106
+ @attributes.slice(*array)
107
+ end
108
+
109
+ def to_h(&block)
110
+ @attributes.to_h(&block)
111
+ end
112
+
113
+ def hash
114
+ to_h.hash
115
+ end
116
+
117
+ def eql?(other)
118
+ self.class == other.class && hash == other.hash
119
+ end
120
+
121
+ def ==(other)
122
+ self.class == other.class && to_h == other.to_h
123
+ end
124
+
125
+ def inspect
126
+ attribute_markers = @attributes.map do |key, value|
127
+ insect_key = key.to_s.start_with?("@") ? ":#{key}" : key
128
+ "#{insect_key}=#{value}"
129
+ end.join(", ")
130
+
131
+ display = ["data", self.class.name, attribute_markers].compact.join(" ")
132
+
133
+ "#<#{display}>"
134
+ end
135
+ alias_method :to_s, :inspect
136
+
137
+ def with(**kwargs)
138
+ return self if kwargs.empty?
139
+
140
+ self.class.new(**@attributes.merge(kwargs))
141
+ end
142
+
143
+ private
144
+
145
+ def marshal_dump
146
+ @attributes
147
+ end
148
+
149
+ def marshal_load(attributes)
150
+ @attributes = attributes
151
+ freeze
152
+ end
153
+
154
+ def initialize_copy(source)
155
+ super.freeze
156
+ end
157
+ end
158
+
159
+ end