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.
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 =