ruby-next-core 0.2.0 → 0.3.0
Sign up to get free protection for your applications and to get access to all the features.
- 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 =
|