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.
- checksums.yaml +4 -4
- data/CHANGELOG.md +26 -0
- data/README.md +69 -7
- data/bin/ruby-next +1 -1
- data/lib/ruby-next/cli.rb +45 -6
- data/lib/ruby-next/commands/core_ext.rb +166 -0
- data/lib/ruby-next/commands/nextify.rb +18 -3
- data/lib/ruby-next/core.rb +149 -1
- data/lib/ruby-next/core/array/deconstruct.rb +21 -0
- data/lib/ruby-next/core/array/difference_union_intersection.rb +15 -21
- data/lib/ruby-next/core/constants/no_matching_pattern_error.rb +17 -0
- data/lib/ruby-next/core/enumerable/filter.rb +20 -18
- data/lib/ruby-next/core/enumerable/filter_map.rb +28 -40
- data/lib/ruby-next/core/enumerable/tally.rb +9 -23
- data/lib/ruby-next/core/enumerator/produce.rb +12 -14
- data/lib/ruby-next/core/hash/deconstruct_keys.rb +21 -0
- data/lib/ruby-next/core/hash/merge.rb +8 -10
- data/lib/ruby-next/core/kernel/then.rb +7 -9
- data/lib/ruby-next/core/proc/compose.rb +12 -14
- data/lib/ruby-next/core/string/split.rb +11 -0
- data/lib/ruby-next/core/struct/deconstruct.rb +7 -0
- data/lib/ruby-next/core/struct/deconstruct_keys.rb +34 -0
- data/lib/ruby-next/core/time/ceil.rb +10 -0
- data/lib/ruby-next/core/time/floor.rb +9 -0
- data/lib/ruby-next/core/unboundmethod/bind_call.rb +9 -0
- data/lib/ruby-next/core_ext.rb +18 -0
- data/lib/ruby-next/language.rb +4 -2
- data/lib/ruby-next/language/parser.rb +5 -1
- data/lib/ruby-next/language/rewriters/base.rb +2 -2
- data/lib/ruby-next/language/rewriters/method_reference.rb +3 -1
- data/lib/ruby-next/language/rewriters/numbered_params.rb +2 -2
- data/lib/ruby-next/language/rewriters/pattern_matching.rb +36 -17
- data/lib/ruby-next/language/runtime.rb +2 -3
- data/lib/ruby-next/version.rb +1 -1
- metadata +13 -3
- 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:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 3c192e6090c23fb8d462dae6403a3e952f661bd0972f49eaaaba064cb2b07084
|
4
|
+
data.tar.gz: c72d83029a820636ce4809e727ad385a5d85bffafee94db2a4764f40eccb36f4
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 26eb5a69b3993cde39121bd19d710d5850f9dc722a807776bdf969b3fc0b52625de05f4f583e10b97fd137bc5c7a108c1c7b4582b8f541d109282c85ed244dc4
|
7
|
+
data.tar.gz: eed1785c806afd65b681f4b22c98658f0e2b43bc20d534ffc1382c476e600b2715d048e1e5409f65c43f92f9913531c1858b6a842348dd172526e1a2391f7865
|
data/CHANGELOG.md
CHANGED
@@ -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
|
-
|
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
|
136
|
-
|
137
|
-
|
138
|
-
|
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.
|
data/bin/ruby-next
CHANGED
data/lib/ruby-next/cli.rb
CHANGED
@@ -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
|
30
|
+
command = extract_command(args)
|
29
31
|
|
30
|
-
|
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
|
-
|
50
|
-
|
51
|
-
|
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
|
-
|
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 =
|