pipe_operator 0.0.1
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +7 -0
- data/.gitignore +4 -0
- data/.rspec +3 -0
- data/.rubocop.yml +116 -0
- data/Gemfile +12 -0
- data/Gemfile.lock +83 -0
- data/LICENSE +20 -0
- data/README.md +384 -0
- data/Rakefile +98 -0
- data/lib/pipe_operator.rb +49 -0
- data/lib/pipe_operator/autoload.rb +3 -0
- data/lib/pipe_operator/closure.rb +66 -0
- data/lib/pipe_operator/observer.rb +13 -0
- data/lib/pipe_operator/pipe.rb +87 -0
- data/lib/pipe_operator/proxy.rb +58 -0
- data/lib/pipe_operator/proxy_resolver.rb +71 -0
- data/pipe_operator.gemspec +22 -0
- data/spec/pipe_operator_spec.rb +243 -0
- data/spec/spec_helper.rb +16 -0
- metadata +71 -0
data/Rakefile
ADDED
@@ -0,0 +1,98 @@
|
|
1
|
+
require "rdoc/task"
|
2
|
+
|
3
|
+
task default: :ci
|
4
|
+
|
5
|
+
desc "scratchpad"
|
6
|
+
task :scratch do
|
7
|
+
require "json"
|
8
|
+
require "net/http"
|
9
|
+
require_relative "lib/pipe_operator/autoload"
|
10
|
+
|
11
|
+
puts "abc".pipe { reverse } #=> "cba"
|
12
|
+
puts "abc".pipe { reverse.upcase } #=> "CBA"
|
13
|
+
|
14
|
+
# puts [9, 64].map(&Math.|.sqrt.to_i)
|
15
|
+
# puts "single"
|
16
|
+
# puts 256.pipe { Math.sqrt.to_i.to_s }.inspect
|
17
|
+
# puts
|
18
|
+
# puts "multiple"
|
19
|
+
# puts [16, 256].map(&Math.|.sqrt.to_i.to_s).inspect
|
20
|
+
|
21
|
+
# "https://api.github.com/repos/ruby/ruby".| do
|
22
|
+
# URI.parse
|
23
|
+
# Net::HTTP.get
|
24
|
+
# JSON.parse.fetch("stargazers_count")
|
25
|
+
# yield_self { |n| "Ruby has #{n} stars" }
|
26
|
+
# Kernel.puts
|
27
|
+
# end
|
28
|
+
# => Ruby has 15115 stars
|
29
|
+
|
30
|
+
# p = ["256", "-16"].pipe do
|
31
|
+
# map(&:to_i)
|
32
|
+
# sort
|
33
|
+
# first
|
34
|
+
# abs
|
35
|
+
# Math.sqrt
|
36
|
+
# to_i
|
37
|
+
# end
|
38
|
+
#
|
39
|
+
# puts p.inspect
|
40
|
+
end
|
41
|
+
task s: :scratch
|
42
|
+
|
43
|
+
desc "run tests, validate styleguide, and generate rdoc"
|
44
|
+
task :ci do
|
45
|
+
%w[lint test doc].each do |task|
|
46
|
+
command = "bundle exec rake #{task} --trace"
|
47
|
+
system(command) || raise("#{task} failed")
|
48
|
+
puts "\n"
|
49
|
+
end
|
50
|
+
end
|
51
|
+
|
52
|
+
desc "validate styleguide"
|
53
|
+
task :lint do
|
54
|
+
%w[fasterer rubocop].each do |task|
|
55
|
+
command = "bundle exec #{task}"
|
56
|
+
system(command) || exit(1)
|
57
|
+
end
|
58
|
+
end
|
59
|
+
task l: :lint
|
60
|
+
|
61
|
+
desc "run tests"
|
62
|
+
task :test do
|
63
|
+
exec "bundle exec rspec"
|
64
|
+
end
|
65
|
+
task t: :test
|
66
|
+
|
67
|
+
|
68
|
+
RDoc::Task.new :doc do |rdoc|
|
69
|
+
rdoc.title = "pipe_operator"
|
70
|
+
|
71
|
+
rdoc.main = "README.md"
|
72
|
+
rdoc.rdoc_dir = "doc"
|
73
|
+
|
74
|
+
rdoc.options << "--all"
|
75
|
+
rdoc.options << "--hyperlink-all"
|
76
|
+
rdoc.options << "--line-numbers"
|
77
|
+
|
78
|
+
rdoc.rdoc_files.include(
|
79
|
+
"LICENSE",
|
80
|
+
"README.md",
|
81
|
+
"lib/**/*.rb",
|
82
|
+
"lib/*.rb"
|
83
|
+
)
|
84
|
+
end
|
85
|
+
task d: :doc
|
86
|
+
|
87
|
+
desc "pry console"
|
88
|
+
task :console do
|
89
|
+
require "base64"
|
90
|
+
require "json"
|
91
|
+
require "net/http"
|
92
|
+
require "pry"
|
93
|
+
require "pry-byebug"
|
94
|
+
require_relative "lib/pipe_operator/autoload"
|
95
|
+
|
96
|
+
PipeOperator.pry
|
97
|
+
end
|
98
|
+
task c: :console
|
@@ -0,0 +1,49 @@
|
|
1
|
+
require "fiddle"
|
2
|
+
require "forwardable"
|
3
|
+
require "pathname"
|
4
|
+
require "set"
|
5
|
+
|
6
|
+
require_relative "pipe_operator/closure"
|
7
|
+
require_relative "pipe_operator/observer"
|
8
|
+
require_relative "pipe_operator/pipe"
|
9
|
+
require_relative "pipe_operator/proxy"
|
10
|
+
require_relative "pipe_operator/proxy_resolver"
|
11
|
+
|
12
|
+
module PipeOperator
|
13
|
+
def __pipe__(*args, &block)
|
14
|
+
Pipe.new(self, *args, &block)
|
15
|
+
end
|
16
|
+
alias | __pipe__
|
17
|
+
alias pipe __pipe__
|
18
|
+
|
19
|
+
refine(::BasicObject) { include PipeOperator }
|
20
|
+
|
21
|
+
class << self
|
22
|
+
def gem
|
23
|
+
@gem ||= ::Gem::Specification.load("#{root}/pipe_operator.gemspec")
|
24
|
+
end
|
25
|
+
|
26
|
+
def inspect(object)
|
27
|
+
object.inspect
|
28
|
+
rescue ::NoMethodError
|
29
|
+
singleton = singleton(object)
|
30
|
+
name = singleton.name || singleton.superclass.name
|
31
|
+
id = "0x0000%x" % (object.__id__ << 1)
|
32
|
+
"#<#{name}:#{id}>"
|
33
|
+
end
|
34
|
+
|
35
|
+
def root
|
36
|
+
@root ||= ::Pathname.new(__dir__).join("..")
|
37
|
+
end
|
38
|
+
|
39
|
+
def singleton(object)
|
40
|
+
(class << object; self end)
|
41
|
+
rescue ::TypeError
|
42
|
+
object.class
|
43
|
+
end
|
44
|
+
|
45
|
+
def version
|
46
|
+
@version ||= gem.version.to_s
|
47
|
+
end
|
48
|
+
end
|
49
|
+
end
|
@@ -0,0 +1,66 @@
|
|
1
|
+
module PipeOperator
|
2
|
+
class Closure < ::Proc
|
3
|
+
RESERVED = %i[
|
4
|
+
==
|
5
|
+
[]
|
6
|
+
__send__
|
7
|
+
call
|
8
|
+
class
|
9
|
+
kind_of?
|
10
|
+
].freeze
|
11
|
+
|
12
|
+
(::Proc.instance_methods - RESERVED).each(&method(:private))
|
13
|
+
|
14
|
+
def self.curry(curry, search, args)
|
15
|
+
index = curry.index(search)
|
16
|
+
prefix = index ? curry[0...index] : curry
|
17
|
+
suffix = index ? curry[index - 1..-1] : []
|
18
|
+
|
19
|
+
(prefix + args + suffix).map do |object|
|
20
|
+
self === object ? object.call : object
|
21
|
+
end
|
22
|
+
end
|
23
|
+
|
24
|
+
def self.new(pipe = nil, method = nil, *curry, &block)
|
25
|
+
return super(&block) unless pipe && method
|
26
|
+
|
27
|
+
search = Pipe.open || pipe
|
28
|
+
|
29
|
+
closure = super() do |*args, &code|
|
30
|
+
code ||= block
|
31
|
+
curried = curry(curry, search, args)
|
32
|
+
value = pipe.__call__.__send__(method, *curried, &code)
|
33
|
+
closure.__chain__(value)
|
34
|
+
end
|
35
|
+
end
|
36
|
+
|
37
|
+
def initialize(*) # :nodoc:
|
38
|
+
@__chain__ ||= []
|
39
|
+
super
|
40
|
+
end
|
41
|
+
|
42
|
+
def __chain__(*args)
|
43
|
+
return @__chain__ if args.empty?
|
44
|
+
|
45
|
+
@__chain__.reduce(args[0]) do |object, chain|
|
46
|
+
method, args, block = chain
|
47
|
+
object.__send__(method, *args, &block)
|
48
|
+
end
|
49
|
+
end
|
50
|
+
|
51
|
+
def __shift__
|
52
|
+
closure = self.class.new do |*args, &block|
|
53
|
+
args.shift
|
54
|
+
value = call(*args, &block)
|
55
|
+
closure.__chain__(value)
|
56
|
+
end
|
57
|
+
end
|
58
|
+
|
59
|
+
private
|
60
|
+
|
61
|
+
def method_missing(method, *args, &block)
|
62
|
+
__chain__ << [method, args, block]
|
63
|
+
self
|
64
|
+
end
|
65
|
+
end
|
66
|
+
end
|
@@ -0,0 +1,13 @@
|
|
1
|
+
module PipeOperator
|
2
|
+
module Observer
|
3
|
+
def singleton_method_added(method)
|
4
|
+
ProxyResolver.new(self).proxy.define(method)
|
5
|
+
super
|
6
|
+
end
|
7
|
+
|
8
|
+
def singleton_method_removed(method)
|
9
|
+
ProxyResolver.new(self).proxy.undefine(method)
|
10
|
+
super
|
11
|
+
end
|
12
|
+
end
|
13
|
+
end
|
@@ -0,0 +1,87 @@
|
|
1
|
+
module PipeOperator
|
2
|
+
class Pipe < ::BasicObject
|
3
|
+
undef :equal?
|
4
|
+
undef :instance_eval
|
5
|
+
undef :singleton_method_added
|
6
|
+
undef :singleton_method_removed
|
7
|
+
undef :singleton_method_undefined
|
8
|
+
|
9
|
+
def self.new(object, *args)
|
10
|
+
if block_given?
|
11
|
+
super.__call__
|
12
|
+
elsif args.none? || Closure === args[0]
|
13
|
+
super
|
14
|
+
else
|
15
|
+
super(object).__send__(*args)
|
16
|
+
end
|
17
|
+
end
|
18
|
+
|
19
|
+
def self.open(pipe = nil)
|
20
|
+
@pipeline ||= []
|
21
|
+
@pipeline << pipe if pipe
|
22
|
+
block_given? ? yield : @pipeline.last
|
23
|
+
ensure
|
24
|
+
@pipeline.pop if pipe
|
25
|
+
end
|
26
|
+
|
27
|
+
def initialize(object, *args, &block)
|
28
|
+
@args = args
|
29
|
+
@block = block
|
30
|
+
@object = object
|
31
|
+
@pipeline = []
|
32
|
+
end
|
33
|
+
|
34
|
+
def __call__
|
35
|
+
if defined?(@pipe)
|
36
|
+
return @pipe
|
37
|
+
elsif @block
|
38
|
+
ProxyResolver.new(::Object).proxy
|
39
|
+
@args.each { |arg| ProxyResolver.new(arg).proxy }
|
40
|
+
Pipe.open(self) { instance_exec(*@args, &@block) }
|
41
|
+
end
|
42
|
+
|
43
|
+
@pipe = @object
|
44
|
+
@pipeline.each { |closure| @pipe = closure.call(@pipe) }
|
45
|
+
@pipe
|
46
|
+
end
|
47
|
+
|
48
|
+
def inspect
|
49
|
+
return method_missing(__method__) if Pipe.open
|
50
|
+
inspect = ::PipeOperator.inspect(@object)
|
51
|
+
"#<#{Pipe.name}:#{inspect}>"
|
52
|
+
end
|
53
|
+
|
54
|
+
protected
|
55
|
+
|
56
|
+
def __pop__(pipe)
|
57
|
+
index = @pipeline.rindex(pipe)
|
58
|
+
@pipeline.delete_at(index) if index
|
59
|
+
end
|
60
|
+
|
61
|
+
def __push__(pipe)
|
62
|
+
@pipeline << pipe
|
63
|
+
pipe
|
64
|
+
end
|
65
|
+
|
66
|
+
def |(*)
|
67
|
+
self
|
68
|
+
end
|
69
|
+
|
70
|
+
private
|
71
|
+
|
72
|
+
def method_missing(method, *curry, &block)
|
73
|
+
closure = Closure.new(self, method, *curry, &block)
|
74
|
+
|
75
|
+
pipe = Pipe.open
|
76
|
+
pipe && [*curry, block].each { |o| pipe.__pop__(o) }
|
77
|
+
|
78
|
+
if pipe == self
|
79
|
+
__push__(closure.__shift__)
|
80
|
+
elsif pipe
|
81
|
+
pipe.__push__(closure)
|
82
|
+
else
|
83
|
+
closure
|
84
|
+
end
|
85
|
+
end
|
86
|
+
end
|
87
|
+
end
|
@@ -0,0 +1,58 @@
|
|
1
|
+
module PipeOperator
|
2
|
+
class Proxy < ::Module
|
3
|
+
def initialize(object, singleton)
|
4
|
+
@object = object if singleton.singleton_class?
|
5
|
+
@singleton = singleton
|
6
|
+
super()
|
7
|
+
end
|
8
|
+
|
9
|
+
def define(method)
|
10
|
+
if ::Proc == @object && method == :new
|
11
|
+
return method
|
12
|
+
elsif ::Symbol == @singleton && method == :to_proc
|
13
|
+
return method
|
14
|
+
elsif ::Module === @object
|
15
|
+
namespace = @object.name.to_s.split("::").first
|
16
|
+
return method if namespace == "PipeOperator"
|
17
|
+
end
|
18
|
+
|
19
|
+
define_method(method) do |*args, &block|
|
20
|
+
if Pipe.open
|
21
|
+
Pipe.new(self).__send__(method, *args, &block)
|
22
|
+
else
|
23
|
+
super(*args, &block)
|
24
|
+
end
|
25
|
+
end
|
26
|
+
end
|
27
|
+
|
28
|
+
def definitions
|
29
|
+
instance_methods(false).sort
|
30
|
+
end
|
31
|
+
|
32
|
+
def inspect
|
33
|
+
inspect =
|
34
|
+
if @singleton.singleton_class?
|
35
|
+
::PipeOperator.inspect(@object)
|
36
|
+
else
|
37
|
+
"#<#{@singleton.name}>"
|
38
|
+
end
|
39
|
+
|
40
|
+
"#<#{self.class.name}:#{inspect}>"
|
41
|
+
end
|
42
|
+
|
43
|
+
def prepended(*)
|
44
|
+
if is_a?(Proxy)
|
45
|
+
methods = @singleton.instance_methods(false)
|
46
|
+
methods.each { |method| define(method) }
|
47
|
+
end
|
48
|
+
|
49
|
+
super
|
50
|
+
end
|
51
|
+
|
52
|
+
def undefine(method)
|
53
|
+
remove_method(method)
|
54
|
+
rescue ::NameError # ignore
|
55
|
+
method
|
56
|
+
end
|
57
|
+
end
|
58
|
+
end
|
@@ -0,0 +1,71 @@
|
|
1
|
+
module PipeOperator
|
2
|
+
class ProxyResolver
|
3
|
+
AUTOLOAD = ENV["PIPE_OPERATOR_AUTOLOAD"] == "1"
|
4
|
+
FROZEN = ENV["PIPE_OPERATOR_FROZEN"] == "1"
|
5
|
+
REBIND = ENV["PIPE_OPERATOR_REBIND"] == "1"
|
6
|
+
|
7
|
+
def initialize(object, resolved = ::Set.new)
|
8
|
+
@object = object
|
9
|
+
@resolved = resolved
|
10
|
+
@singleton = ::PipeOperator.singleton(object)
|
11
|
+
end
|
12
|
+
|
13
|
+
def proxy
|
14
|
+
proxy = find_existing_proxy
|
15
|
+
return proxy if proxy && !REBIND
|
16
|
+
proxy ||= create_proxy
|
17
|
+
rebind_nested_constants
|
18
|
+
proxy
|
19
|
+
end
|
20
|
+
|
21
|
+
private
|
22
|
+
|
23
|
+
def find_existing_proxy
|
24
|
+
@singleton.ancestors.each do |existing|
|
25
|
+
break if @singleton == existing
|
26
|
+
return existing if Proxy === existing
|
27
|
+
end
|
28
|
+
end
|
29
|
+
|
30
|
+
def create_proxy
|
31
|
+
Proxy.new(@object, @singleton).tap do |proxy|
|
32
|
+
@resolved.add(proxy)
|
33
|
+
|
34
|
+
if !@singleton.frozen?
|
35
|
+
@singleton.prepend(Observer).prepend(proxy)
|
36
|
+
elsif FROZEN
|
37
|
+
id = @singleton.__id__ * 2
|
38
|
+
unfreeze = ~(1 << 3)
|
39
|
+
::Fiddle::Pointer.new(id)[1] &= unfreeze
|
40
|
+
@singleton.prepend(Observer).prepend(proxy)
|
41
|
+
@singleton.freeze
|
42
|
+
end
|
43
|
+
end
|
44
|
+
end
|
45
|
+
|
46
|
+
def rebind_nested_constants
|
47
|
+
context = ::Module === @object ? @object : @singleton
|
48
|
+
|
49
|
+
context.constants.map do |constant|
|
50
|
+
next unless context.const_defined?(constant, AUTOLOAD)
|
51
|
+
|
52
|
+
constant = silence_deprecations do
|
53
|
+
context.const_get(constant, false) rescue next
|
54
|
+
end
|
55
|
+
|
56
|
+
next if constant.eql?(@object) # recursion
|
57
|
+
next unless @resolved.add?(constant)
|
58
|
+
|
59
|
+
self.class.new(constant, @resolved).proxy
|
60
|
+
end
|
61
|
+
end
|
62
|
+
|
63
|
+
def silence_deprecations
|
64
|
+
stderr = $stderr
|
65
|
+
$stderr = ::StringIO.new
|
66
|
+
yield
|
67
|
+
ensure
|
68
|
+
$stderr = stderr
|
69
|
+
end
|
70
|
+
end
|
71
|
+
end
|