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
checksums.yaml
ADDED
@@ -0,0 +1,7 @@
|
|
1
|
+
---
|
2
|
+
SHA256:
|
3
|
+
metadata.gz: fc08d5b15937d1418ca864e631eb9f0a6a3fafd04e2c1f7496fbdd925531eec7
|
4
|
+
data.tar.gz: 340f185be7fe2f803a0d61603f1b1c89c641f6dfbf21ba691e1c3e48c3c5bdce
|
5
|
+
SHA512:
|
6
|
+
metadata.gz: 4b2e3096c092d839dde50eadbc196cef82745371c99cb313f845587b0c4908ab6225a261309984d2cdef927605f44308026adf57a7c02970721d84c0e240d958
|
7
|
+
data.tar.gz: a873ca59f03c6e50630c9d8b91f5494a5b7bfb06b558b21c574c716065f63fb381d155b66c106c4d12de237fbb8b1506bb40fd2e459f130a278ceea26ee0daa5
|
data/.gitignore
ADDED
data/.rspec
ADDED
data/.rubocop.yml
ADDED
@@ -0,0 +1,116 @@
|
|
1
|
+
AllCops:
|
2
|
+
DisplayCopNames: true
|
3
|
+
TargetRubyVersion: 2.5
|
4
|
+
Exclude:
|
5
|
+
- "Rakefile"
|
6
|
+
- "tmp/**/*"
|
7
|
+
|
8
|
+
Gemspec/RequiredRubyVersion:
|
9
|
+
Enabled: false
|
10
|
+
|
11
|
+
Layout/AlignHash:
|
12
|
+
Exclude:
|
13
|
+
- "spec/**/*"
|
14
|
+
|
15
|
+
Layout/EmptyLineAfterGuardClause:
|
16
|
+
Enabled: false
|
17
|
+
|
18
|
+
Layout/SpaceBeforeBlockBraces:
|
19
|
+
Exclude:
|
20
|
+
- "spec/**/*"
|
21
|
+
|
22
|
+
Layout/SpaceInsideBlockBraces:
|
23
|
+
Exclude:
|
24
|
+
- "spec/**/*"
|
25
|
+
|
26
|
+
Metrics/AbcSize:
|
27
|
+
Enabled: false
|
28
|
+
|
29
|
+
Metrics/BlockLength:
|
30
|
+
Enabled: false
|
31
|
+
|
32
|
+
Metrics/CyclomaticComplexity:
|
33
|
+
Enabled: false
|
34
|
+
|
35
|
+
Metrics/LineLength:
|
36
|
+
Enabled: false
|
37
|
+
|
38
|
+
Metrics/MethodLength:
|
39
|
+
Enabled: false
|
40
|
+
|
41
|
+
Metrics/PerceivedComplexity:
|
42
|
+
Exclude:
|
43
|
+
- "lib/pipe_operator/proxy.rb"
|
44
|
+
- "lib/pipe_operator/proxy_resolver.rb"
|
45
|
+
|
46
|
+
Naming/MemoizedInstanceVariableName:
|
47
|
+
Exclude:
|
48
|
+
- "lib/pipe_operator/closure.rb"
|
49
|
+
|
50
|
+
Naming/UncommunicativeMethodParamName:
|
51
|
+
Enabled: false
|
52
|
+
|
53
|
+
Style/BlockDelimiters:
|
54
|
+
Exclude:
|
55
|
+
- "spec/**/*"
|
56
|
+
|
57
|
+
Style/CaseEquality:
|
58
|
+
Enabled: false
|
59
|
+
|
60
|
+
Style/Documentation:
|
61
|
+
Enabled: false
|
62
|
+
|
63
|
+
Style/DoubleNegation:
|
64
|
+
Enabled: false
|
65
|
+
|
66
|
+
Style/FormatString:
|
67
|
+
Enabled: false
|
68
|
+
|
69
|
+
Style/FormatStringToken:
|
70
|
+
Enabled: false
|
71
|
+
|
72
|
+
Style/FrozenStringLiteralComment:
|
73
|
+
Enabled: false
|
74
|
+
|
75
|
+
Style/GuardClause:
|
76
|
+
Enabled: false
|
77
|
+
|
78
|
+
Style/LambdaCall:
|
79
|
+
Exclude:
|
80
|
+
- "spec/**/*"
|
81
|
+
|
82
|
+
Style/MethodMissingSuper:
|
83
|
+
Exclude:
|
84
|
+
- "lib/pipe_operator/closure.rb"
|
85
|
+
- "lib/pipe_operator/pipe.rb"
|
86
|
+
|
87
|
+
Style/RedundantBegin:
|
88
|
+
Exclude:
|
89
|
+
- "lib/pipe_operator/proxy_resolver.rb"
|
90
|
+
|
91
|
+
Style/RescueModifier:
|
92
|
+
Exclude:
|
93
|
+
- "lib/pipe_operator/proxy_resolver.rb"
|
94
|
+
|
95
|
+
Style/MissingRespondToMissing:
|
96
|
+
Exclude:
|
97
|
+
- "lib/pipe_operator/closure.rb"
|
98
|
+
- "lib/pipe_operator/pipe.rb"
|
99
|
+
|
100
|
+
Style/Semicolon:
|
101
|
+
Exclude:
|
102
|
+
- "spec/**/*"
|
103
|
+
|
104
|
+
Style/SingleLineMethods:
|
105
|
+
Exclude:
|
106
|
+
- "spec/**/*"
|
107
|
+
|
108
|
+
Style/StringLiterals:
|
109
|
+
EnforcedStyle: double_quotes
|
110
|
+
|
111
|
+
Style/TrailingCommaInHashLiteral:
|
112
|
+
Exclude:
|
113
|
+
- "spec/**/*"
|
114
|
+
|
115
|
+
Style/WordArray:
|
116
|
+
Enabled: false
|
data/Gemfile
ADDED
data/Gemfile.lock
ADDED
@@ -0,0 +1,83 @@
|
|
1
|
+
PATH
|
2
|
+
remote: .
|
3
|
+
specs:
|
4
|
+
pipe_operator (0.0.1)
|
5
|
+
|
6
|
+
GEM
|
7
|
+
remote: https://www.rubygems.org/
|
8
|
+
specs:
|
9
|
+
ast (2.4.0)
|
10
|
+
byebug (10.0.2)
|
11
|
+
codeclimate-test-reporter (1.0.9)
|
12
|
+
simplecov (<= 0.13)
|
13
|
+
coderay (1.1.2)
|
14
|
+
colorize (0.8.1)
|
15
|
+
diff-lcs (1.3)
|
16
|
+
docile (1.1.5)
|
17
|
+
fasterer (0.4.1)
|
18
|
+
colorize (~> 0.7)
|
19
|
+
ruby_parser (~> 3.11.0)
|
20
|
+
jaro_winkler (1.5.1)
|
21
|
+
json (2.1.0)
|
22
|
+
method_source (0.9.2)
|
23
|
+
parallel (1.12.1)
|
24
|
+
parser (2.5.3.0)
|
25
|
+
ast (~> 2.4.0)
|
26
|
+
powerpack (0.1.2)
|
27
|
+
pry (0.12.2)
|
28
|
+
coderay (~> 1.1.0)
|
29
|
+
method_source (~> 0.9.0)
|
30
|
+
pry-byebug (3.6.0)
|
31
|
+
byebug (~> 10.0)
|
32
|
+
pry (~> 0.10)
|
33
|
+
rainbow (3.0.0)
|
34
|
+
rake (12.3.1)
|
35
|
+
rdoc (6.0.4)
|
36
|
+
rspec (3.8.0)
|
37
|
+
rspec-core (~> 3.8.0)
|
38
|
+
rspec-expectations (~> 3.8.0)
|
39
|
+
rspec-mocks (~> 3.8.0)
|
40
|
+
rspec-core (3.8.0)
|
41
|
+
rspec-support (~> 3.8.0)
|
42
|
+
rspec-expectations (3.8.2)
|
43
|
+
diff-lcs (>= 1.2.0, < 2.0)
|
44
|
+
rspec-support (~> 3.8.0)
|
45
|
+
rspec-mocks (3.8.0)
|
46
|
+
diff-lcs (>= 1.2.0, < 2.0)
|
47
|
+
rspec-support (~> 3.8.0)
|
48
|
+
rspec-support (3.8.0)
|
49
|
+
rubocop (0.60.0)
|
50
|
+
jaro_winkler (~> 1.5.1)
|
51
|
+
parallel (~> 1.10)
|
52
|
+
parser (>= 2.5, != 2.5.1.1)
|
53
|
+
powerpack (~> 0.1)
|
54
|
+
rainbow (>= 2.2.2, < 4.0)
|
55
|
+
ruby-progressbar (~> 1.7)
|
56
|
+
unicode-display_width (~> 1.4.0)
|
57
|
+
ruby-progressbar (1.10.0)
|
58
|
+
ruby_parser (3.11.0)
|
59
|
+
sexp_processor (~> 4.9)
|
60
|
+
sexp_processor (4.11.0)
|
61
|
+
simplecov (0.13.0)
|
62
|
+
docile (~> 1.1.0)
|
63
|
+
json (>= 1.8, < 3)
|
64
|
+
simplecov-html (~> 0.10.0)
|
65
|
+
simplecov-html (0.10.2)
|
66
|
+
unicode-display_width (1.4.0)
|
67
|
+
|
68
|
+
PLATFORMS
|
69
|
+
ruby
|
70
|
+
|
71
|
+
DEPENDENCIES
|
72
|
+
codeclimate-test-reporter
|
73
|
+
fasterer
|
74
|
+
pipe_operator!
|
75
|
+
pry
|
76
|
+
pry-byebug
|
77
|
+
rake
|
78
|
+
rdoc
|
79
|
+
rspec
|
80
|
+
rubocop
|
81
|
+
|
82
|
+
BUNDLED WITH
|
83
|
+
1.17.1
|
data/LICENSE
ADDED
@@ -0,0 +1,20 @@
|
|
1
|
+
Copyright (c) 2018 LendingHome - engineering@lendinghome.com
|
2
|
+
|
3
|
+
Permission is hereby granted, free of charge, to any person obtaining
|
4
|
+
a copy of this software and associated documentation files (the
|
5
|
+
"Software"), to deal in the Software without restriction, including
|
6
|
+
without limitation the rights to use, copy, modify, merge, publish,
|
7
|
+
distribute, sublicense, and/or sell copies of the Software, and to
|
8
|
+
permit persons to whom the Software is furnished to do so, subject to
|
9
|
+
the following conditions:
|
10
|
+
|
11
|
+
The above copyright notice and this permission notice shall be
|
12
|
+
included in all copies or substantial portions of the Software.
|
13
|
+
|
14
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
15
|
+
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
|
16
|
+
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
|
17
|
+
NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
|
18
|
+
LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
|
19
|
+
OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
|
20
|
+
WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
data/README.md
ADDED
@@ -0,0 +1,384 @@
|
|
1
|
+
#  pipe_operator
|
2
|
+
|
3
|
+
> Elixir/Unix style pipe operations in Ruby - **PROOF OF CONCEPT**
|
4
|
+
|
5
|
+
```ruby
|
6
|
+
-9.pipe { abs | Math.sqrt | to_i } #=> 3
|
7
|
+
```
|
8
|
+
|
9
|
+
```ruby
|
10
|
+
[9, 64].map(&Math.|.sqrt.to_i) #=> [3, 8]
|
11
|
+
```
|
12
|
+
|
13
|
+
```ruby
|
14
|
+
"https://api.github.com/repos/ruby/ruby".| do
|
15
|
+
URI.parse
|
16
|
+
Net::HTTP.get
|
17
|
+
JSON.parse.fetch("stargazers_count")
|
18
|
+
yield_self { |n| "Ruby has #{n} stars" }
|
19
|
+
Kernel.puts
|
20
|
+
end
|
21
|
+
#=> Ruby has 15115 stars
|
22
|
+
```
|
23
|
+
|
24
|
+
## Why?
|
25
|
+
|
26
|
+
There's been some recent activity related to `Method` and `Proc` composition in Ruby:
|
27
|
+
|
28
|
+
* [#6284 - Add composition for procs](https://bugs.ruby-lang.org/issues/6284)
|
29
|
+
* [#13581 - Syntax sugar for method reference](https://bugs.ruby-lang.org/issues/13581)
|
30
|
+
* [#12125 - Shorthand operator for Object#method](https://bugs.ruby-lang.org/issues/12125)
|
31
|
+
|
32
|
+
This gem was created to **propose an alternative syntax** for this kind of behavior.
|
33
|
+
|
34
|
+
## Concept
|
35
|
+
|
36
|
+
The general idea is to **pass the result of one expression as an argument to another expression** - similar to [Unix pipelines](https://en.wikipedia.org/wiki/Pipeline_(Unix)):
|
37
|
+
|
38
|
+
```ruby
|
39
|
+
echo "testing" | sed "s/ing//" | rev
|
40
|
+
#=> tset
|
41
|
+
```
|
42
|
+
|
43
|
+
The [Elixir pipe operator documentation](https://elixirschool.com/en/lessons/basics/pipe-operator/) has some other examples but basically it allows expressions like:
|
44
|
+
|
45
|
+
```ruby
|
46
|
+
JSON.parse(Net::HTTP.get(URI.parse(url)))
|
47
|
+
```
|
48
|
+
|
49
|
+
To be **inversed** and rewritten as **left to right** or **top to bottom** which is more **natural to read** in English:
|
50
|
+
|
51
|
+
```ruby
|
52
|
+
# left to right
|
53
|
+
url.pipe { URI.parse | Net::HTTP.get | JSON.parse }
|
54
|
+
|
55
|
+
# or top to bottom for clarity
|
56
|
+
url.pipe do
|
57
|
+
URI.parse
|
58
|
+
Net::HTTP.get
|
59
|
+
JSON.parse
|
60
|
+
end
|
61
|
+
```
|
62
|
+
|
63
|
+
The differences become a bit **clearer when other arguments are involved**:
|
64
|
+
|
65
|
+
```ruby
|
66
|
+
loans = Loan.preapproved.submitted(Date.current).where(broker: Current.user)
|
67
|
+
data = loans.map { |loan| LoanPresenter.new(loan).as_json }
|
68
|
+
json = JSON.pretty_generate(data, allow_nan: false)
|
69
|
+
```
|
70
|
+
|
71
|
+
Using pipes **removes the verbosity of maps and temporary variables**:
|
72
|
+
|
73
|
+
```ruby
|
74
|
+
json = Loan.pipe do
|
75
|
+
preapproved
|
76
|
+
submitted(Date.current)
|
77
|
+
where(broker: Current.user)
|
78
|
+
map(&LoanPresenter.new.as_json)
|
79
|
+
JSON.pretty_generate(allow_nan: false)
|
80
|
+
end
|
81
|
+
```
|
82
|
+
|
83
|
+
While the ability to perform a job correctly and efficiently is certainly important - the **true beauty of a program lies in its clarity and conciseness**:
|
84
|
+
|
85
|
+
```ruby
|
86
|
+
"https://api.github.com/repos/ruby/ruby".| do
|
87
|
+
URI.parse
|
88
|
+
Net::HTTP.get
|
89
|
+
JSON.parse.fetch("stargazers_count")
|
90
|
+
yield_self { |n| "Ruby has #{n} stars" }
|
91
|
+
Kernel.puts
|
92
|
+
end
|
93
|
+
#=> Ruby has 15115 stars
|
94
|
+
```
|
95
|
+
|
96
|
+
There's nothing really special here - it's just a **block of expressions like any other Ruby DSL** and the pipe `|` operator has been [around for decades](https://en.wikipedia.org/wiki/Pipeline_(Unix))!
|
97
|
+
|
98
|
+
```ruby
|
99
|
+
# we can already do this
|
100
|
+
objects.map(&Marshal.method(:dump))
|
101
|
+
|
102
|
+
# but this is more concise
|
103
|
+
objects.map(&Marshal.|.dump)
|
104
|
+
```
|
105
|
+
|
106
|
+
The **simplicity and elegance of Ruby** is one of the many reasons that people fall in love with the language!
|
107
|
+
|
108
|
+
```ruby
|
109
|
+
Ruby.is.so(:simple, &elegant).that(you can) do
|
110
|
+
pretty_much ANYTHING if it.compiles!
|
111
|
+
end
|
112
|
+
```
|
113
|
+
|
114
|
+
This concept of **pipes could be a great fit** like it has been for many other languages:
|
115
|
+
|
116
|
+
* [Caml composition operators](http://caml.inria.fr/pub/docs/manual-ocaml/libref/Pervasives.html#1_Compositionoperators)
|
117
|
+
* [Closure threading macros](https://clojure.org/guides/threading_macros)
|
118
|
+
* [Elixir pipe operator](https://elixirschool.com/en/lessons/basics/pipe-operator/)
|
119
|
+
* [Elm operators](https://elm-lang.org/docs/syntax#operators)
|
120
|
+
* [F# function composition and pipelining](https://docs.microsoft.com/en-us/dotnet/fsharp/language-reference/functions/index#function-composition-and-pipelining)
|
121
|
+
* [Hack pipe operator](https://docs.hhvm.com/hack/operators/pipe-operator)
|
122
|
+
* [Haskell pipes](http://hackage.haskell.org/package/pipes-4.3.9/docs/Pipes-Tutorial.html)
|
123
|
+
* [JavaScript pipeline operator proposals](https://github.com/tc39/proposal-pipeline-operator/wiki)
|
124
|
+
* [LiveScript piping](http://livescript.net/#piping)
|
125
|
+
* [Unix pipelines](https://en.wikipedia.org/wiki/Pipeline_(Unix))
|
126
|
+
|
127
|
+
## Usage
|
128
|
+
|
129
|
+
**WARNING - EXPERIMENTAL PROOF OF CONCEPT**
|
130
|
+
|
131
|
+
This has only been **tested in isolation with RSpec**!
|
132
|
+
|
133
|
+
```ruby
|
134
|
+
# First `gem install pipe_operator`
|
135
|
+
require "pipe_operator"
|
136
|
+
|
137
|
+
# Then use PipeOperator as a refinement
|
138
|
+
using ::PipeOperator
|
139
|
+
|
140
|
+
# Or include PipeOperator in classes/modules
|
141
|
+
class AnyClass
|
142
|
+
include ::PipeOperator
|
143
|
+
end
|
144
|
+
|
145
|
+
# Or monkey patch PipeOperator into ALL objects
|
146
|
+
require "pipe_operator/autoload"
|
147
|
+
```
|
148
|
+
|
149
|
+
## Implementation
|
150
|
+
|
151
|
+
The [PipeOperator](https://github.com/lendinghome/pipe_operator/blob/master/lib/pipe_operator.rb) module has a method named `__pipe__` which is aliased as `pipe` for convenience and `|` for syntactic sugar:
|
152
|
+
|
153
|
+
```ruby
|
154
|
+
def __pipe__(*args, &block)
|
155
|
+
Pipe.new(self, *args, &block)
|
156
|
+
end
|
157
|
+
|
158
|
+
alias | __pipe__
|
159
|
+
alias pipe __pipe__
|
160
|
+
```
|
161
|
+
|
162
|
+
When no arguments are passed to `__pipe__` then a [PipeOperator::Pipe](https://github.com/lendinghome/pipe_operator/blob/master/lib/pipe_operator/pipe.rb) object is returned:
|
163
|
+
|
164
|
+
```ruby
|
165
|
+
Math.| #=> #<PipeOperator::Pipe:Math>
|
166
|
+
```
|
167
|
+
|
168
|
+
Any methods invoked on this object returns a [PipeOperator::Closure](https://github.com/lendinghome/pipe_operator/blob/master/lib/pipe_operator/closure.rb) which **calls the method on the object later**:
|
169
|
+
|
170
|
+
```ruby
|
171
|
+
sqrt = Math.|.sqrt #=> #<PipeOperator::Closure:0x00007fc1172ed558@pipe_operator/closure.rb:18>
|
172
|
+
sqrt.call(16) #=> 4.0
|
173
|
+
|
174
|
+
missing = Math.|.missing #=> #<PipeOperator::Closure:0x00007fc11726f0e0@pipe_operator/closure.rb:18>
|
175
|
+
missing.call #=> NoMethodError: undefined method 'missing' for Math:Module
|
176
|
+
|
177
|
+
Math.method(:missing) #=> NameError: undefined method 'missing' for class '#<Class:Math>'
|
178
|
+
```
|
179
|
+
|
180
|
+
When `__pipe__` is called **with arguments but without a block** then it behaves similar to `__send__`:
|
181
|
+
|
182
|
+
```ruby
|
183
|
+
sqrt = Math | :sqrt #=> #<PipeOperator::Closure:0x00007fe52e0cdf80@pipe_operator/closure.rb:18>
|
184
|
+
sqrt.call(16) #=> 4.0
|
185
|
+
|
186
|
+
sqrt = Math.pipe(:sqrt, 16) #=> #<PipeOperator::Closure:0x00007fe52fa18fd0@pipe_operator/closure.rb:18>
|
187
|
+
sqrt.call #=> 4.0
|
188
|
+
sqrt.call(16) #=> ArgumentError: wrong number of arguments (given 2, expected 1)
|
189
|
+
```
|
190
|
+
|
191
|
+
These [PipeOperator::Closure](https://github.com/lendinghome/pipe_operator/blob/master/lib/pipe_operator/closure.rb) objects can be [bound as block arguments](https://github.com/lendinghome/pipe_operator/blob/master/lib/pipe_operator/proxy.rb#L10-L13) just like any other [Proc](https://ruby-doc.org/core-2.5.3/Proc.html):
|
192
|
+
|
193
|
+
```ruby
|
194
|
+
[16, 256].map(&Math.|.sqrt) #=> [4.0, 16.0]
|
195
|
+
```
|
196
|
+
|
197
|
+
Simple **closure composition is supported** via [method chaining](https://github.com/lendinghome/pipe_operator/blob/master/lib/pipe_operator/closure.rb#L56):
|
198
|
+
|
199
|
+
```ruby
|
200
|
+
[16, 256].map(&Math.|.sqrt.to_i.to_s) #=> ["4", "16"]
|
201
|
+
```
|
202
|
+
|
203
|
+
The **block** form of `__pipe__` behaves **similar to instance_exec** but can also [call methods on other objects](https://github.com/lendinghome/pipe_operator/blob/master/lib/pipe_operator/pipe.rb#L81):
|
204
|
+
|
205
|
+
```ruby
|
206
|
+
"abc".pipe { reverse } #=> "cba"
|
207
|
+
"abc".pipe { reverse.upcase } #=> "CBA"
|
208
|
+
|
209
|
+
"abc".pipe { Marshal.dump } #=> "\x04\bI\"\babc\x06:\x06ET"
|
210
|
+
"abc".pipe { Marshal.dump | Base64.encode64 } #=> "BAhJIghhYmMGOgZFVA==\n"
|
211
|
+
```
|
212
|
+
|
213
|
+
Outside the context of a `__pipe__` block things behave like normal:
|
214
|
+
|
215
|
+
```ruby
|
216
|
+
Math.sqrt #=> ArgumentError: wrong number of arguments (given 0, expected 1)
|
217
|
+
Math.sqrt(16) #=> 4.0
|
218
|
+
```
|
219
|
+
|
220
|
+
But within a `__pipe__` block the `Math.sqrt` expression returns a [PipeOperator::Closure](https://github.com/lendinghome/pipe_operator/blob/master/lib/pipe_operator/closure.rb) instead:
|
221
|
+
|
222
|
+
```ruby
|
223
|
+
16.pipe { Math.sqrt } #=> 4.0
|
224
|
+
16.pipe { Math.sqrt(16) } #=> ArgumentError: wrong number of arguments (given 2, expected 1)
|
225
|
+
```
|
226
|
+
|
227
|
+
The **piped object is passed as the first argument by default** but can be customized by specifying `self`:
|
228
|
+
|
229
|
+
```ruby
|
230
|
+
class String
|
231
|
+
def self.join(*args, with: "")
|
232
|
+
args.map(&:to_s).join(with)
|
233
|
+
end
|
234
|
+
end
|
235
|
+
|
236
|
+
"test".pipe { String.join("123", with: "-") } #=> "test-123"
|
237
|
+
|
238
|
+
"test".pipe { String.join("123", self, with: "-") } #=> "123-test"
|
239
|
+
```
|
240
|
+
|
241
|
+
Instance methods like `reverse` below [do not receive the piped object](https://github.com/lendinghome/pipe_operator/blob/master/lib/pipe_operator/pipe.rb#L79) as [an argument](https://github.com/lendinghome/pipe_operator/blob/master/lib/pipe_operator/closure.rb#L47) since it's available as `self`:
|
242
|
+
|
243
|
+
```ruby
|
244
|
+
Base64.encode64(Marshal.dump("abc").reverse) #=> "VEUGOgZjYmEIIkkIBA==\n"
|
245
|
+
|
246
|
+
"abc".pipe { Marshal.dump | reverse | Base64.encode64 } #=> "VEUGOgZjYmEIIkkIBA==\n"
|
247
|
+
```
|
248
|
+
|
249
|
+
Pipes also support **multi-line blocks for clarity**:
|
250
|
+
|
251
|
+
```ruby
|
252
|
+
"abc".pipe do
|
253
|
+
Marshal.dump
|
254
|
+
reverse
|
255
|
+
Base64.encode64
|
256
|
+
end
|
257
|
+
```
|
258
|
+
|
259
|
+
Notice the pipe `|` operator wasn't used to **separate expressions** - it's actually always optional:
|
260
|
+
|
261
|
+
```ruby
|
262
|
+
# this example from above
|
263
|
+
"abc".pipe { Marshal.dump | reverse | Base64.encode64 }
|
264
|
+
|
265
|
+
# could also be written as
|
266
|
+
"abc".pipe { Marshal.dump; reverse; Base64.encode64 }
|
267
|
+
```
|
268
|
+
|
269
|
+
The closures created by these **pipe expressions are evaluated via reduce**:
|
270
|
+
|
271
|
+
```ruby
|
272
|
+
pipeline = [
|
273
|
+
-> object { Marshal.dump(object) },
|
274
|
+
-> object { object.reverse },
|
275
|
+
-> object { Base64.encode64(object) },
|
276
|
+
]
|
277
|
+
|
278
|
+
pipeline.reduce("abc") do |object, pipe|
|
279
|
+
pipe.call(object)
|
280
|
+
end
|
281
|
+
```
|
282
|
+
|
283
|
+
[Intercepting methods](https://github.com/lendinghome/pipe_operator/blob/master/lib/pipe_operator/proxy.rb#L19-L25) within pipes requires [prepending](https://github.com/lendinghome/pipe_operator/blob/master/lib/pipe_operator/pipe.rb#L38) a [PipeOperator::Proxy](https://github.com/lendinghome/pipe_operator/blob/master/lib/pipe_operator/proxy.rb) module infront of `::Object` and all [nested constants](https://github.com/lendinghome/pipe_operator/blob/master/lib/pipe_operator/proxy_resolver.rb#L46):
|
284
|
+
|
285
|
+
```ruby
|
286
|
+
define_method(method) do |*args, &block|
|
287
|
+
if Pipe.open
|
288
|
+
Pipe.new(self).__send__(method, *args, &block)
|
289
|
+
else
|
290
|
+
super(*args, &block)
|
291
|
+
end
|
292
|
+
end
|
293
|
+
```
|
294
|
+
|
295
|
+
These proxy modules are prepended everywhere! It's certainly something that **could be way more efficient as a core part of Ruby**.
|
296
|
+
|
297
|
+
## Bugs
|
298
|
+
|
299
|
+
This test case doesn't work yet - seems like the [object is not proxied](https://github.com/lendinghome/pipe_operator/blob/master/lib/pipe_operator/pipe.rb#L39) for some reason:
|
300
|
+
|
301
|
+
```ruby
|
302
|
+
class Markdown
|
303
|
+
def format(string)
|
304
|
+
string.upcase
|
305
|
+
end
|
306
|
+
end
|
307
|
+
|
308
|
+
"test".pipe(Markdown.new, &:format) # expected "TEST"
|
309
|
+
#=> ArgumentError: wrong number of arguments (given 0, expected 1)
|
310
|
+
```
|
311
|
+
|
312
|
+
## Caveats
|
313
|
+
|
314
|
+
* `PIPE_OPERATOR_AUTOLOAD`
|
315
|
+
* Constants flagged for autoload are NOT proxied by default (for performance)
|
316
|
+
* Set `ENV["PIPE_OPERATOR_AUTOLOAD"] = 1` to enable this behavior
|
317
|
+
* `PIPE_OPERATOR_FROZEN`
|
318
|
+
* Objects flagged as frozen are NOT proxied by default
|
319
|
+
* Set `ENV["PIPE_OPERATOR_FROZEN"] = 1` to enable this behavior (via [Fiddle](http://ruby-doc.org/stdlib-2.5.3/libdoc/fiddle/rdoc/Fiddle.html))
|
320
|
+
* `PIPE_OPERATOR_REBIND`
|
321
|
+
* `Object` and its recursively nested `constants` are only proxied ONCE by default (for performance)
|
322
|
+
* Constants defined after `__pipe__` is called for the first time are NOT proxied
|
323
|
+
* Set `ENV["PIPE_OPERATOR_REBIND"] = 1` to enable this behavior
|
324
|
+
* `PIPE_OPERATOR_RESERVED`
|
325
|
+
* The following methods are reserved on `PipeOperator::Closure` objects:
|
326
|
+
* `==`
|
327
|
+
* `[]`
|
328
|
+
* `__chain__`
|
329
|
+
* `__send__`
|
330
|
+
* `__shift__`
|
331
|
+
* `call`
|
332
|
+
* `class`
|
333
|
+
* `kind_of?`
|
334
|
+
* The following methods are reserved on `PipeOperator::Pipe` objects:
|
335
|
+
* `!`
|
336
|
+
* `!=`
|
337
|
+
* `==`
|
338
|
+
* `__call__`
|
339
|
+
* `__id__`
|
340
|
+
* `__pop__`
|
341
|
+
* `__push__`
|
342
|
+
* `__send__`
|
343
|
+
* `instance_exec`
|
344
|
+
* `method_missing`
|
345
|
+
* `|`
|
346
|
+
* These methods can be piped via `send` as a workaround:
|
347
|
+
* `9.pipe { Math.sqrt.to_s.send(:[], 0) }`
|
348
|
+
* `example.pipe { send(:__call__, 1, 2, 3) }`
|
349
|
+
* `example.pipe { send(:instance_exec) { } }`
|
350
|
+
|
351
|
+
## Testing
|
352
|
+
|
353
|
+
```bash
|
354
|
+
bundle exec rspec
|
355
|
+
```
|
356
|
+
|
357
|
+
## Inspiration
|
358
|
+
|
359
|
+
* https://github.com/hopsoft/pipe_envy
|
360
|
+
* https://github.com/akitaonrails/chainable_methods
|
361
|
+
* https://github.com/kek/pipelining
|
362
|
+
* https://github.com/k-motoyan/shelike-pipe
|
363
|
+
* https://github.com/nwtgck/ruby_pipe_chain
|
364
|
+
* https://github.com/teamsnap/pipe-ruby
|
365
|
+
* https://github.com/danielpclark/elixirize
|
366
|
+
* https://github.com/tiagopog/piped_ruby
|
367
|
+
* https://github.com/jicksta/methodphitamine
|
368
|
+
* https://github.com/jicksta/superators
|
369
|
+
|
370
|
+
## Contributing
|
371
|
+
|
372
|
+
* Fork the project.
|
373
|
+
* Make your feature addition or bug fix.
|
374
|
+
* Add tests for it. This is important so we don't break it in a future version unintentionally.
|
375
|
+
* Commit, do not mess with the version or history.
|
376
|
+
* Open a pull request. Bonus points for topic branches.
|
377
|
+
|
378
|
+
## Authors
|
379
|
+
|
380
|
+
* [Sean Huber](https://github.com/shuber)
|
381
|
+
|
382
|
+
## License
|
383
|
+
|
384
|
+
[MIT](https://github.com/lendinghome/pipe_operator/blob/master/LICENSE) - Copyright © 2018 LendingHome
|