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

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
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