pipe_operator 0.0.1
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 +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
|