ruby-next-core 0.2.0 → 0.3.0

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 (36) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +26 -0
  3. data/README.md +69 -7
  4. data/bin/ruby-next +1 -1
  5. data/lib/ruby-next/cli.rb +45 -6
  6. data/lib/ruby-next/commands/core_ext.rb +166 -0
  7. data/lib/ruby-next/commands/nextify.rb +18 -3
  8. data/lib/ruby-next/core.rb +149 -1
  9. data/lib/ruby-next/core/array/deconstruct.rb +21 -0
  10. data/lib/ruby-next/core/array/difference_union_intersection.rb +15 -21
  11. data/lib/ruby-next/core/constants/no_matching_pattern_error.rb +17 -0
  12. data/lib/ruby-next/core/enumerable/filter.rb +20 -18
  13. data/lib/ruby-next/core/enumerable/filter_map.rb +28 -40
  14. data/lib/ruby-next/core/enumerable/tally.rb +9 -23
  15. data/lib/ruby-next/core/enumerator/produce.rb +12 -14
  16. data/lib/ruby-next/core/hash/deconstruct_keys.rb +21 -0
  17. data/lib/ruby-next/core/hash/merge.rb +8 -10
  18. data/lib/ruby-next/core/kernel/then.rb +7 -9
  19. data/lib/ruby-next/core/proc/compose.rb +12 -14
  20. data/lib/ruby-next/core/string/split.rb +11 -0
  21. data/lib/ruby-next/core/struct/deconstruct.rb +7 -0
  22. data/lib/ruby-next/core/struct/deconstruct_keys.rb +34 -0
  23. data/lib/ruby-next/core/time/ceil.rb +10 -0
  24. data/lib/ruby-next/core/time/floor.rb +9 -0
  25. data/lib/ruby-next/core/unboundmethod/bind_call.rb +9 -0
  26. data/lib/ruby-next/core_ext.rb +18 -0
  27. data/lib/ruby-next/language.rb +4 -2
  28. data/lib/ruby-next/language/parser.rb +5 -1
  29. data/lib/ruby-next/language/rewriters/base.rb +2 -2
  30. data/lib/ruby-next/language/rewriters/method_reference.rb +3 -1
  31. data/lib/ruby-next/language/rewriters/numbered_params.rb +2 -2
  32. data/lib/ruby-next/language/rewriters/pattern_matching.rb +36 -17
  33. data/lib/ruby-next/language/runtime.rb +2 -3
  34. data/lib/ruby-next/version.rb +1 -1
  35. metadata +13 -3
  36. data/lib/ruby-next/core/pattern_matching.rb +0 -37
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 920bab371a964915799cae3423ebe43f77d54986a904da19e9c868efe309fc42
4
- data.tar.gz: a19414277a1cc30b1f7863d765026b2c20162d51ae6aadd84def479915a641bd
3
+ metadata.gz: 3c192e6090c23fb8d462dae6403a3e952f661bd0972f49eaaaba064cb2b07084
4
+ data.tar.gz: c72d83029a820636ce4809e727ad385a5d85bffafee94db2a4764f40eccb36f4
5
5
  SHA512:
6
- metadata.gz: c72c42d6ef93f871448e50d15d935001973347450b6cfc47eef3c455ffcbfee18c33e03e761315571fa4010535c0649b82882b38e951229d6141f82b45fe01af
7
- data.tar.gz: 1ba4bac7b1a9c18abfc07fb9304b92393ffa61659e80d4cb99d91c606569331a3ca0cd35eb9cd64a7078dbd905cdf417abf35b24ca7742dc8eb466625d53a3f2
6
+ metadata.gz: 26eb5a69b3993cde39121bd19d710d5850f9dc722a807776bdf969b3fc0b52625de05f4f583e10b97fd137bc5c7a108c1c7b4582b8f541d109282c85ed244dc4
7
+ data.tar.gz: eed1785c806afd65b681f4b22c98658f0e2b43bc20d534ffc1382c476e600b2715d048e1e5409f65c43f92f9913531c1858b6a842348dd172526e1a2391f7865
@@ -2,6 +2,32 @@
2
2
 
3
3
  ## master
4
4
 
5
+ ## 0.3.0 (2020-02-14) 💕
6
+
7
+ - Add `Time#floor` and `Time#ceil`. ([@palkan][])
8
+
9
+ - Add `UnboundMethod#bind_call`. ([@palkan][])
10
+
11
+ - Add `String#split` with block. ([@palkan][])
12
+
13
+ - **Check for _native_ method implementation to activate a refinement.** ([@palkan][])
14
+
15
+ Add a method refinement to `using RubyNext` even if the backport is present. That
16
+ helps to avoid the conflicts with invalid monkey-patches.
17
+
18
+ - Add `ruby-next core_ext` command. ([@palkan][])
19
+
20
+ This command allows generating custom core extension files. Meant to be used in
21
+ alternative Ruby implementations (mruby, Opal, etc.) not compatible with the `ruby-next-core` gem.
22
+
23
+ - Add `ruby-next/core_ext`. ([@palkan][])
24
+
25
+ Now you can use core extensions (monkey-patches) instead of the refinements.
26
+
27
+ - Check whether pattern matching target respond to `#deconstruct` / `#deconstruct_keys`. ([@palkan][])
28
+
29
+ - Fix `Struct#deconstruct_keys` to respect passed keys. ([@palkan][])
30
+
5
31
  ## 0.2.0 (2020-01-13) 🎄
6
32
 
7
33
  - Add `Enumerator.produce`. ([@palkan][])
data/README.md CHANGED
@@ -26,8 +26,7 @@ _⚡️ The project is in a **beta** phase. That means that the main functionali
26
26
 
27
27
  Ruby Next consists of two parts: **core** and **language**.
28
28
 
29
- Core provides **polyfills** for Ruby core classes APIs via Refinements.
30
- Thus, polyfills are only available in compatible runtimes (MRI, JRuby, TruffleRuby).
29
+ Core provides **polyfills** for Ruby core classes APIs via Refinements (default strategy) or core extensions (optionally or for refinement-less environments).
31
30
 
32
31
  Language is responsible for **transpiling** edge Ruby syntax into older versions. It could be done
33
32
  programmatically or via CLI. It also could be done in runtime.
@@ -63,6 +62,20 @@ using RubyNext
63
62
 
64
63
  Ruby Next only refines core classes if necessary; thus, this line wouldn't have any effect in the edge Ruby.
65
64
 
65
+ **NOTE:** Even if the runtime already contains a monkey-patch with the backported functionality, we consider the method as _dirty_ and activate the refinement for it. Thus, you always have a predictable behaviour. That's why we recommend using refinements for gems development.
66
+
67
+ Alternatively, you can go with monkey-patches. Just add this line:
68
+
69
+ ```ruby
70
+ require "ruby-next/core_ext"
71
+ ```
72
+
73
+ The following _rule of thumb_ is recommended when choosing between refinements and monkey-patches:
74
+
75
+ - Use refinements for libraries development (to avoid conflicts with others code)
76
+ - Using core extensions could be considered for application development (no need to think about `using RubyNext`); this approach could potentially lead to conflicts with dependendices (if these dependencies are not using refinements 🙂)
77
+ - Use core extensions if refinements are not supported by your platform
78
+
66
79
  [**The list of supported APIs.**][features_core]
67
80
 
68
81
  ## Transpiling, or using edge Ruby syntax features
@@ -125,17 +138,22 @@ due to the way feature resolving works in Ruby (scanning the `$LOAD_PATH` and ha
125
138
 
126
139
  Ruby Next ships with the command-line interface (`ruby-next`) which provides the following functionality:
127
140
 
128
- - `ruby-next nextify` — transpile file or directory into older Rubies (see, for example, the "Integrating into a gem development" section above).
141
+ ### `ruby-next nextify`
142
+
143
+ This command allows you to transpile a file or directory into older Rubies (see, for example, the "Integrating into a gem development" section above).
129
144
 
130
145
  It has the following interface:
131
146
 
132
147
  ```sh
133
148
  $ ruby-next nextify
134
149
  Usage: ruby-next nextify DIRECTORY_OR_FILE [options]
135
- -o, --output=OUTPUT Specify output directory or file
136
- --min-version=VERSION Specify the minimum Ruby version to support
137
- --single-version Only create one version of a file (for the earliest Ruby version)
138
- -V Turn on verbose mode
150
+ -o, --output=OUTPUT Specify output directory or file or stdout (use -o stdout for that)
151
+ --min-version=VERSION Specify the minimum Ruby version to support
152
+ --single-version Only create one version of a file (for the earliest Ruby version)
153
+ --enable-method-reference Enable reverted method reference syntax (requires custom parser)
154
+ --[no-]refine Do not inject `using RubyNext`
155
+ -h, --help Print help
156
+ -V Turn on verbose mode
139
157
  ```
140
158
 
141
159
  The behaviour depends on whether you transpile a single file or a directory:
@@ -146,9 +164,53 @@ The behaviour depends on whether you transpile a single file or a directory:
146
164
 
147
165
  ```sh
148
166
  $ ruby-next nextify my_ruby.rb -o my_ruby_next.rb -V
167
+ RubyNext core strategy: refine
149
168
  Generated: my_ruby_next.rb
150
169
  ```
151
170
 
171
+ ### `ruby-next core_ext`
172
+
173
+ This command could be used to generate a Ruby file with a configurable set of core extensions.
174
+
175
+ Use this command if you want to backport new Ruby features to Ruby implementations not compatible with RubyGems.
176
+
177
+ It has the following interface:
178
+
179
+ ```sh
180
+ $ ruby-next core_ext
181
+ Usage: ruby-next core_ext [options]
182
+ -o, --output=OUTPUT Specify output file or stdout (default: ./core_ext.rb)
183
+ -l, --list List all available extensions
184
+ --min-version=VERSION Specify the minimum Ruby version to support
185
+ -n, --name=NAME Filter extensions by name
186
+ -h, --help Print help
187
+ -V Turn on verbose mode
188
+ ```
189
+
190
+ The most common usecase is to backport the APIs required by pattern matching. You can do this, for example,
191
+ by including only monkey-patches containing the `"deconstruct"` in their names:
192
+
193
+ ```sh
194
+ ruby-next core_ext -n deconstruct -o pattern_matching_core_ext.rb
195
+ ```
196
+
197
+ To list all available (are matching if `--min-version` or `--name` specified) monkey-patches, use the `-l` switch:
198
+
199
+ ```sh
200
+ $ ruby-next core_ext -l --name=filter --name=deconstruct
201
+ 2.6 extensions:
202
+ - ArrayFilter
203
+ - EnumerableFilter
204
+ - HashFilter
205
+
206
+ 2.7 extensions:
207
+ - ArrayDeconstruct
208
+ - EnumerableFilterMap
209
+ - EnumeratorLazyFilterMap
210
+ - HashDeconstructKeys
211
+ - StructDeconstruct
212
+ ```
213
+
152
214
  ## Runtime mode
153
215
 
154
216
  It is also possible to transpile Ruby source code in run-time via Ruby Next.
@@ -11,6 +11,6 @@ begin
11
11
  rescue => e
12
12
  raise e if $DEBUG
13
13
  STDERR.puts e.message
14
- STDERR.puts e.backtrace.join("\n")
14
+ STDERR.puts e.backtrace.join("\n") if ENV["RUBY_NEXT_DEBUG"] == "1"
15
15
  exit 1
16
16
  end
@@ -5,6 +5,7 @@ require "ruby-next/language"
5
5
 
6
6
  require "ruby-next/commands/base"
7
7
  require "ruby-next/commands/nextify"
8
+ require "ruby-next/commands/core_ext"
8
9
 
9
10
  module RubyNext
10
11
  # Command line interface for RubyNext
@@ -16,7 +17,8 @@ module RubyNext
16
17
  self.verbose = false
17
18
 
18
19
  COMMANDS = {
19
- "nextify" => Commands::Nextify
20
+ "nextify" => Commands::Nextify,
21
+ "core_ext" => Commands::CoreExt
20
22
  }.freeze
21
23
 
22
24
  def initialize
@@ -25,9 +27,15 @@ module RubyNext
25
27
  def run(args = ARGV)
26
28
  maybe_print_version(args)
27
29
 
28
- command = args.shift
30
+ command = extract_command(args)
29
31
 
30
- raise "Command must be specified!" unless command
32
+ # Handle top-level help
33
+ unless command
34
+ maybe_print_help
35
+ raise "Command must be specified!"
36
+ end
37
+
38
+ args.delete(command)
31
39
 
32
40
  COMMANDS.fetch(command) do
33
41
  raise "Unknown command: #{command}. Available commands: #{COMMANDS.keys.join(",")}"
@@ -39,6 +47,35 @@ module RubyNext
39
47
  def maybe_print_version(args)
40
48
  args = args.dup
41
49
  begin
50
+ optparser.parse!(args)
51
+ rescue OptionParser::InvalidOption
52
+ # skip and pass all args to the command's parser
53
+ end
54
+ end
55
+
56
+ def maybe_print_help
57
+ return unless @print_help
58
+
59
+ STDOUT.puts optparser.help
60
+ exit 0
61
+ end
62
+
63
+ def extract_command(source_args)
64
+ args = source_args.dup
65
+ unknown_args = []
66
+ command = nil
67
+ begin
68
+ command, = optparser.permute!(args)
69
+ rescue OptionParser::InvalidOption => e
70
+ unknown_args += e.args
71
+ args = source_args - unknown_args
72
+ retry
73
+ end
74
+ command
75
+ end
76
+
77
+ def optparser
78
+ @optparser ||= begin
42
79
  OptionParser.new do |opts|
43
80
  opts.banner = "Usage: ruby-next COMMAND [options]"
44
81
 
@@ -46,9 +83,11 @@ module RubyNext
46
83
  STDOUT.puts RubyNext::VERSION
47
84
  exit 0
48
85
  end
49
- end.parse!(args)
50
- rescue OptionParser::InvalidOption
51
- # skip and pass all args to the command's parser
86
+
87
+ opts.on("-h", "--help", "Print help") do
88
+ @print_help = true
89
+ end
90
+ end
52
91
  end
53
92
  end
54
93
  end
@@ -0,0 +1,166 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "fileutils"
4
+ require "pathname"
5
+
6
+ module RubyNext
7
+ module Commands
8
+ class CoreExt < Base
9
+ using RubyNext
10
+
11
+ attr_reader :out_path, :min_version, :names, :list, :filter, :original_command
12
+ alias list? list
13
+
14
+ def run
15
+ log "Select core extensions for Ruby v#{min_version}" \
16
+ "#{filter ? " and matching #{filter.inspect}" : ""}"
17
+
18
+ matching_patches.then do |patches|
19
+ next print_list(patches) if list?
20
+ generate_core_ext(patches)
21
+ end
22
+ end
23
+
24
+ def parse!(args)
25
+ print_help = false
26
+ @min_version = MIN_SUPPORTED_VERSION
27
+ @original_command = "ruby-next core_ext #{args.join(" ")}"
28
+ @names = []
29
+ @list = false
30
+ @out_path = File.join(Dir.pwd, "core_ext.rb")
31
+
32
+ optparser = base_parser do |opts|
33
+ opts.banner = "Usage: ruby-next core_ext [options]"
34
+
35
+ opts.on("-o", "--output=OUTPUT", "Specify output file or stdout (default: ./core_ext.rb)") do |val|
36
+ @out_path = val
37
+ end
38
+
39
+ opts.on("-l", "--list", "List all available extensions") do
40
+ @list = true
41
+ end
42
+
43
+ opts.on("--min-version=VERSION", "Specify the minimum Ruby version to support") do |val|
44
+ @min_version = Gem::Version.new(val)
45
+ end
46
+
47
+ opts.on("-n", "--name=NAME", "Filter extensions by name") do |val|
48
+ names << val
49
+ end
50
+
51
+ opts.on("-h", "--help", "Print help") do
52
+ print_help = true
53
+ end
54
+ end
55
+
56
+ optparser.parse!(args)
57
+
58
+ if print_help
59
+ $stdout.puts optparser.help
60
+ exit 0
61
+ end
62
+
63
+ @filter = /(#{names.join("|")})/i unless names.empty?
64
+ end
65
+
66
+ private
67
+
68
+ def matching_patches
69
+ RubyNext::Core.patches.extensions
70
+ .values
71
+ .flatten
72
+ .filter do |patch|
73
+ next if min_version && Gem::Version.new(patch.version) <= min_version
74
+ next if filter && !filter.match?(patch.name)
75
+ true
76
+ end
77
+ end
78
+
79
+ def print_list(patches)
80
+ grouped_patches = patches.group_by(&:version).sort_by(&:first)
81
+ grouped_patches.each do |(group, patches)|
82
+ $stdout.puts "#{group} extensions:\n"
83
+ $stdout.puts patches.sort_by(&:name).map { |patch| " - #{patch.name}" }.join("\n")
84
+ $stdout.puts "\n"
85
+ end
86
+ end
87
+
88
+ def generate_core_ext(patches)
89
+ grouped_patches = patches.group_by(&:mod).sort_by { |(mod, patch)| mod.singleton_class? ? mod.inspect : mod.name }
90
+
91
+ buffer = []
92
+
93
+ buffer << "# frozen_string_literal: true\n"
94
+
95
+ buffer << generation_meta
96
+
97
+ grouped_patches.each do |mod, patches|
98
+ singleton = mod.singleton_class?
99
+ extend_name = singleton ? patches.first.singleton.name : mod.name
100
+ prepend_name = singleton ? "#{patches.first.singleton.name}.singleton_class" : mod.name
101
+
102
+ prepended, extended = patches.partition(&:prepend?)
103
+
104
+ prepended.map do |patch|
105
+ name = "RubyNext::Core::#{patch.name}"
106
+
107
+ buffer << <<~RUBY
108
+ module #{name}
109
+ #{indent_and_trim(patch.body)}
110
+ end
111
+
112
+ #{prepend_name}.prepend #{name}
113
+ RUBY
114
+
115
+ name
116
+ end
117
+
118
+ class_or_module = mod.is_a?(Class) ? "class" : "module"
119
+
120
+ buffer << "#{class_or_module} #{extend_name}"
121
+
122
+ buffer << " class << self" if singleton
123
+
124
+ indent_size = singleton ? 4 : 2
125
+
126
+ buffer << extended.map do |patch|
127
+ indent_and_trim(patch.body, indent_size)
128
+ end.join("\n\n")
129
+
130
+ buffer << " end" if singleton
131
+
132
+ buffer << "end\n"
133
+ end
134
+
135
+ contents = buffer.join("\n")
136
+
137
+ return $stdout.puts(contents) if out_path == "stdout"
138
+
139
+ FileUtils.mkdir_p File.dirname(out_path)
140
+ File.write(out_path, contents)
141
+
142
+ log "Generated: #{out_path}"
143
+ end
144
+
145
+ def generation_meta
146
+ <<~MSG
147
+ # Generated by RubyNext v#{RubyNext::VERSION} using the following command:
148
+ #
149
+ # #{original_command}
150
+ #
151
+ MSG
152
+ end
153
+
154
+ def indent_and_trim(src, size = 2)
155
+ new_src = src.dup
156
+ # indent code using <size> spaces
157
+ new_src.gsub!(/^/, " " * size)
158
+ # remove empty lines
159
+ new_src.gsub!(/^\s+$/, "")
160
+ # remove traling blank lines
161
+ new_src.gsub!(/\n\z/, "")
162
+ new_src
163
+ end
164
+ end
165
+ end
166
+ end
@@ -3,14 +3,15 @@
3
3
  require "fileutils"
4
4
  require "pathname"
5
5
 
6
- using RubyNext
7
-
8
6
  module RubyNext
9
7
  module Commands
10
8
  class Nextify < Base
9
+ using RubyNext
10
+
11
11
  attr_reader :lib_path, :paths, :out_path, :min_version, :single_version
12
12
 
13
13
  def run
14
+ log "RubyNext core strategy: #{RubyNext::Core.strategy}"
14
15
  paths.each do |path|
15
16
  contents = File.read(path)
16
17
  transpile path, contents
@@ -18,6 +19,7 @@ module RubyNext
18
19
  end
19
20
 
20
21
  def parse!(args)
22
+ print_help = false
21
23
  @min_version = MIN_SUPPORTED_VERSION
22
24
  @single_version = false
23
25
 
@@ -40,15 +42,28 @@ module RubyNext
40
42
  require "ruby-next/language/rewriters/method_reference"
41
43
  Language.rewriters << Language::Rewriters::MethodReference
42
44
  end
45
+
46
+ opts.on("--[no-]refine", "Do not inject `using RubyNext`") do |val|
47
+ Core.strategy = :core_ext unless val
48
+ end
49
+
50
+ opts.on("-h", "--help", "Print help") do
51
+ print_help = true
52
+ end
43
53
  end
44
54
 
45
55
  @lib_path = args[0]
46
56
 
47
- unless lib_path&.then(&File.method(:exist?))
57
+ if print_help
48
58
  $stdout.puts optparser.help
49
59
  exit 0
50
60
  end
51
61
 
62
+ unless lib_path&.then(&File.method(:exist?))
63
+ $stdout.puts optparser.help
64
+ exit 2
65
+ end
66
+
52
67
  optparser.parse!(args)
53
68
 
54
69
  @paths =