├── .gitignore ├── .rspec ├── .rubocop.yml ├── Gemfile ├── Gemfile.lock ├── LICENSE ├── README.md ├── Rakefile ├── lib ├── pipe_operator.rb └── pipe_operator │ ├── closure.rb │ ├── observer.rb │ ├── pipe.rb │ ├── proxy.rb │ └── proxy_resolver.rb ├── pipe_operator.gemspec └── spec ├── pipe_operator_spec.rb └── spec_helper.rb /.gitignore: -------------------------------------------------------------------------------- 1 | coverage 2 | doc 3 | pkg 4 | rdoc 5 | -------------------------------------------------------------------------------- /.rspec: -------------------------------------------------------------------------------- 1 | --color 2 | --format progress 3 | --require spec_helper 4 | -------------------------------------------------------------------------------- /.rubocop.yml: -------------------------------------------------------------------------------- 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 117 | -------------------------------------------------------------------------------- /Gemfile: -------------------------------------------------------------------------------- 1 | source "https://www.rubygems.org" 2 | 3 | gemspec 4 | 5 | gem "codeclimate-test-reporter" 6 | gem "fasterer" 7 | gem "pry" 8 | gem "pry-byebug" 9 | gem "rake" 10 | gem "rdoc" 11 | gem "rspec" 12 | gem "rubocop" 13 | -------------------------------------------------------------------------------- /Gemfile.lock: -------------------------------------------------------------------------------- 1 | PATH 2 | remote: . 3 | specs: 4 | pipe_operator (0.0.2) 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 84 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 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. 21 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # ![LendingHome](https://avatars0.githubusercontent.com/u/5448482?s=24&v=4) pipe_operator 2 | 3 | > Elixir/Unix style pipe operations in Ruby - **PROOF OF CONCEPT** 4 | 5 | ```ruby 6 | "https://api.github.com/repos/ruby/ruby".pipe do 7 | URI.parse 8 | Net::HTTP.get 9 | JSON.parse.fetch("stargazers_count") 10 | yield_self { |n| "Ruby has #{n} stars" } 11 | Kernel.puts 12 | end 13 | #=> Ruby has 15120 stars 14 | ``` 15 | 16 | ```ruby 17 | -9.pipe { abs; Math.sqrt; to_i } #=> 3 18 | 19 | # Method chaining is supported: 20 | -9.pipe { abs; Math.sqrt.to_i.to_s } #=> "3" 21 | ``` 22 | 23 | ```ruby 24 | sqrt = Math.pipe.sqrt #=> # 25 | sqrt.call(9) #=> 3.0 26 | sqrt.call(64) #=> 8.0 27 | 28 | [9, 64].map(&Math.pipe.sqrt) #=> [3.0, 8.0] 29 | [9, 64].map(&Math.pipe.sqrt.to_i.to_s) #=> ["3", "8"] 30 | ``` 31 | 32 | ## Why? 33 | 34 | There's been some recent activity related to `Method` and `Proc` composition in Ruby: 35 | 36 | * [#6284 - Add composition for procs](https://bugs.ruby-lang.org/issues/6284) 37 | * [#13581 - Syntax sugar for method reference](https://bugs.ruby-lang.org/issues/13581) 38 | * [#12125 - Shorthand operator for Object#method](https://bugs.ruby-lang.org/issues/12125) 39 | 40 | This gem was created to **propose an alternative syntax** for this kind of behavior. 41 | 42 | ## Matz on Ruby 43 | 44 | Source: [ruby-lang.org/en/about](https://www.ruby-lang.org/en/about) 45 | 46 | Ruby is a language of careful **balance of both functional and imperative programming**. 47 | 48 | Matz has often said that he is **trying to make Ruby natural, not simple**, in a way that mirrors life. 49 | 50 | Building on this, he adds: Ruby is **simple in appearance, but is very complex inside**, just like our human body. 51 | 52 | ## Concept 53 | 54 | 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)): 55 | 56 | ```ruby 57 | echo "testing" | sed "s/ing//" | rev 58 | #=> tset 59 | ``` 60 | 61 | The [Elixir pipe operator documentation](https://elixirschool.com/en/lessons/basics/pipe-operator/) has some other examples but basically it allows expressions like: 62 | 63 | ```ruby 64 | JSON.parse(Net::HTTP.get(URI.parse(url))) 65 | ``` 66 | 67 | To be **inverted** and rewritten as **left to right** or **top to bottom** which is more **natural to read** in English: 68 | 69 | ```ruby 70 | # left to right 71 | url.pipe { URI.parse; Net::HTTP.get; JSON.parse } 72 | 73 | # or top to bottom for clarity 74 | url.pipe do 75 | URI.parse 76 | Net::HTTP.get 77 | JSON.parse 78 | end 79 | ``` 80 | 81 | The differences become a bit **clearer when other arguments are involved**: 82 | 83 | ```ruby 84 | loans = Loan.preapproved.submitted(Date.current).where(broker: Current.user) 85 | data = loans.map { |loan| LoanPresenter.new(loan).as_json } 86 | json = JSON.pretty_generate(data, allow_nan: false) 87 | ``` 88 | 89 | Using pipes **removes the verbosity of maps and temporary variables**: 90 | 91 | ```ruby 92 | json = Loan.pipe do 93 | preapproved 94 | submitted(Date.current) 95 | where(broker: Current.user) 96 | map(&LoanPresenter.new.as_json) 97 | JSON.pretty_generate(allow_nan: false) 98 | end 99 | ``` 100 | 101 | 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**: 102 | 103 | ```ruby 104 | "https://api.github.com/repos/ruby/ruby".pipe do 105 | URI.parse 106 | Net::HTTP.get 107 | JSON.parse.fetch("stargazers_count") 108 | yield_self { |n| "Ruby has #{n} stars" } 109 | Kernel.puts 110 | end 111 | #=> Ruby has 15115 stars 112 | ``` 113 | 114 | There's nothing really special here - it's just a **block of expressions like any other Ruby DSL** and pipe operations have been [around for decades](https://en.wikipedia.org/wiki/Pipeline_(Unix))! 115 | 116 | ```ruby 117 | Ruby.is.so(elegant, &:expressive).that(you can) do 118 | pretty_much ANYTHING if it.compiles! 119 | end 120 | ``` 121 | 122 | This concept of **pipe operations could be a great fit** like it has been for many other languages: 123 | 124 | * [Caml composition operators](http://caml.inria.fr/pub/docs/manual-ocaml/libref/Pervasives.html#1_Compositionoperators) 125 | * [Closure threading macros](https://clojure.org/guides/threading_macros) 126 | * [Elixir pipe operator](https://elixirschool.com/en/lessons/basics/pipe-operator/) 127 | * [Elm operators](https://elm-lang.org/docs/syntax#operators) 128 | * [F# function composition and pipelining](https://docs.microsoft.com/en-us/dotnet/fsharp/language-reference/functions/index#function-composition-and-pipelining) 129 | * [Hack pipe operator](https://docs.hhvm.com/hack/operators/pipe-operator) 130 | * [Haskell pipes](http://hackage.haskell.org/package/pipes-4.3.9/docs/Pipes-Tutorial.html) 131 | * [JavaScript pipeline operator proposals](https://github.com/tc39/proposal-pipeline-operator/wiki) 132 | * [LiveScript piping](http://livescript.net/#piping) 133 | * [Unix pipelines](https://en.wikipedia.org/wiki/Pipeline_(Unix)) 134 | 135 | ## Usage 136 | 137 | **WARNING - EXPERIMENTAL PROOF OF CONCEPT** 138 | 139 | This has only been **tested in isolation with RSpec and Ruby 2.5.3**! 140 | 141 | ```ruby 142 | # First `gem install pipe_operator` 143 | require "pipe_operator" 144 | ``` 145 | 146 | ## Implementation 147 | 148 | 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: 149 | 150 | ```ruby 151 | module PipeOperator 152 | def __pipe__(*args, &block) 153 | Pipe.new(self, *args, &block) 154 | end 155 | end 156 | 157 | BasicObject.send(:include, PipeOperator) 158 | Kernel.alias_method(:pipe, :__pipe__) 159 | ``` 160 | 161 | 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: 162 | 163 | ```ruby 164 | Math.pipe #=> # 165 | ``` 166 | 167 | 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**: 168 | 169 | ```ruby 170 | sqrt = Math.pipe.sqrt #=> # 171 | sqrt.call(16) #=> 4.0 172 | 173 | missing = Math.pipe.missing #=> # 174 | missing.call #=> NoMethodError: undefined method 'missing' for Math:Module 175 | 176 | Math.method(:missing) #=> NameError: undefined method 'missing' for class '#' 177 | ``` 178 | 179 | When `__pipe__` is called **with arguments but without a block** then it behaves similar to `__send__`: 180 | 181 | ```ruby 182 | sqrt = Math.pipe(:sqrt) #=> # 183 | sqrt.call(16) #=> 4.0 184 | 185 | sqrt = Math.pipe(:sqrt, 16) #=> # 186 | sqrt.call #=> 4.0 187 | sqrt.call(16) #=> ArgumentError: wrong number of arguments (given 2, expected 1) 188 | ``` 189 | 190 | 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): 191 | 192 | ```ruby 193 | [16, 256].map(&Math.pipe.sqrt) #=> [4.0, 16.0] 194 | ``` 195 | 196 | Simple **closure composition is supported** via [method chaining](https://github.com/lendinghome/pipe_operator/blob/master/lib/pipe_operator/closure.rb#L56): 197 | 198 | ```ruby 199 | [16, 256].map(&Math.pipe.sqrt.to_i.to_s) #=> ["4", "16"] 200 | ``` 201 | 202 | 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): 203 | 204 | ```ruby 205 | "abc".pipe { reverse } #=> "cba" 206 | "abc".pipe { reverse.upcase } #=> "CBA" 207 | 208 | "abc".pipe { Marshal.dump } #=> "\x04\bI\"\babc\x06:\x06ET" 209 | "abc".pipe { Marshal.dump; Base64.encode64 } #=> "BAhJIghhYmMGOgZFVA==\n" 210 | ``` 211 | 212 | Outside the context of a `__pipe__` block things behave like normal: 213 | 214 | ```ruby 215 | Math.sqrt #=> ArgumentError: wrong number of arguments (given 0, expected 1) 216 | Math.sqrt(16) #=> 4.0 217 | ``` 218 | 219 | 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: 220 | 221 | ```ruby 222 | 16.pipe { Math.sqrt } #=> 4.0 223 | 16.pipe { Math.sqrt(16) } #=> ArgumentError: wrong number of arguments (given 2, expected 1) 224 | ``` 225 | 226 | The **piped object is passed as the first argument by default** but can be customized by specifying `self`: 227 | 228 | ```ruby 229 | class String 230 | def self.join(*args, with: "") 231 | args.map(&:to_s).join(with) 232 | end 233 | end 234 | 235 | "test".pipe { String.join("123", with: "-") } #=> "test-123" 236 | 237 | "test".pipe { String.join("123", self, with: "-") } #=> "123-test" 238 | ``` 239 | 240 | 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`: 241 | 242 | ```ruby 243 | Base64.encode64(Marshal.dump("abc").reverse) #=> "VEUGOgZjYmEIIkkIBA==\n" 244 | 245 | "abc".pipe { Marshal.dump; reverse; Base64.encode64 } #=> "VEUGOgZjYmEIIkkIBA==\n" 246 | 247 | "abc".pipe { Marshal.dump.reverse; Base64.encode64 } #=> "VEUGOgZjYmEIIkkIBA==\n" 248 | ``` 249 | 250 | Pipes also support **multi-line blocks for clarity**: 251 | 252 | ```ruby 253 | "abc".pipe do 254 | Marshal.dump.reverse 255 | Base64.encode64 256 | end 257 | ``` 258 | 259 | The closures created by these **pipe expressions are evaluated via reduce**: 260 | 261 | ```ruby 262 | pipeline = [ 263 | -> object { Marshal.dump(object) }, 264 | -> object { object.reverse }, 265 | -> object { Base64.encode64(object) }, 266 | ] 267 | 268 | pipeline.reduce("abc") do |object, pipe| 269 | pipe.call(object) 270 | end 271 | ``` 272 | 273 | [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): 274 | 275 | ```ruby 276 | define_method(method) do |*args, &block| 277 | if Pipe.open 278 | Pipe.new(self).__send__(method, *args, &block) 279 | else 280 | super(*args, &block) 281 | end 282 | end 283 | ``` 284 | 285 | These **proxy modules are prepended everywhere**! 286 | 287 | It's certainly something that **could be way more efficient as a core part of Ruby**. 288 | 289 | Maybe somewhere **lower level where methods are dispatched**? Possibly somewhere in this [vm_eval.c switch](https://github.com/ruby/ruby/blob/trunk/vm_eval.c#L111)? 290 | 291 | ```c 292 | again: 293 | switch (cc->me->def->type) { 294 | case VM_METHOD_TYPE_ISEQ 295 | case VM_METHOD_TYPE_NOTIMPLEMENTED 296 | case VM_METHOD_TYPE_CFUNC 297 | case VM_METHOD_TYPE_ATTRSET 298 | case VM_METHOD_TYPE_IVAR 299 | case VM_METHOD_TYPE_BMETHOD 300 | case VM_METHOD_TYPE_ZSUPER 301 | case VM_METHOD_TYPE_REFINED 302 | case VM_METHOD_TYPE_ALIAS 303 | case VM_METHOD_TYPE_MISSING 304 | case VM_METHOD_TYPE_OPTIMIZED 305 | case OPTIMIZED_METHOD_TYPE_SEND 306 | case OPTIMIZED_METHOD_TYPE_CALL 307 | case VM_METHOD_TYPE_UNDEF 308 | } 309 | ``` 310 | 311 | Then we'd **only need Ruby C API ports** for [PipeOperator::Pipe](https://github.com/LendingHome/pipe_operator/blob/master/lib/pipe_operator/pipe.rb) and [PipeOperator::Closure](https://github.com/LendingHome/pipe_operator/blob/master/lib/pipe_operator/closure.rb)! 312 | 313 | All other objects in this proof of concept are related to **method interception** and would no longer be necessary. 314 | 315 | ## Bugs 316 | 317 | 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: 318 | 319 | ```ruby 320 | class Markdown 321 | def format(string) 322 | string.upcase 323 | end 324 | end 325 | 326 | "test".pipe(Markdown.new, &:format) # expected "TEST" 327 | #=> ArgumentError: wrong number of arguments (given 0, expected 1) 328 | ``` 329 | 330 | ## Caveats 331 | 332 | * `PIPE_OPERATOR_AUTOLOAD` 333 | * Constants flagged for autoload are NOT proxied by default (for performance) 334 | * Set `ENV["PIPE_OPERATOR_AUTOLOAD"] = 1` to enable this behavior 335 | * `PIPE_OPERATOR_FROZEN` 336 | * Objects flagged as frozen are NOT proxied by default 337 | * 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)) 338 | * `PIPE_OPERATOR_REBIND` 339 | * `Object` and its recursively nested `constants` are only proxied ONCE by default (for performance) 340 | * Constants defined after `__pipe__` is called for the first time are NOT proxied 341 | * Set `ENV["PIPE_OPERATOR_REBIND"] = 1` to enable this behavior 342 | * `PIPE_OPERATOR_RESERVED` 343 | * The following methods are reserved on `PipeOperator::Closure` objects: 344 | * `==` 345 | * `[]` 346 | * `__chain__` 347 | * `__send__` 348 | * `__shift__` 349 | * `call` 350 | * `class` 351 | * `kind_of?` 352 | * The following methods are reserved on `PipeOperator::Pipe` objects: 353 | * `!` 354 | * `!=` 355 | * `==` 356 | * `__call__` 357 | * `__id__` 358 | * `__pop__` 359 | * `__push__` 360 | * `__send__` 361 | * `instance_exec` 362 | * `method_missing` 363 | * These methods can be piped via `send` as a workaround: 364 | * `9.pipe { Math.sqrt.to_s.send(:[], 0) }` 365 | * `example.pipe { send(:__call__, 1, 2, 3) }` 366 | * `example.pipe { send(:instance_exec) { } }` 367 | 368 | ## Testing 369 | 370 | ```bash 371 | bundle exec rspec 372 | ``` 373 | 374 | ## Inspiration 375 | 376 | * https://github.com/hopsoft/pipe_envy 377 | * https://github.com/akitaonrails/chainable_methods 378 | * https://github.com/kek/pipelining 379 | * https://github.com/k-motoyan/shelike-pipe 380 | * https://github.com/nwtgck/ruby_pipe_chain 381 | * https://github.com/teamsnap/pipe-ruby 382 | * https://github.com/danielpclark/elixirize 383 | * https://github.com/tiagopog/piped_ruby 384 | * https://github.com/jicksta/methodphitamine 385 | * https://github.com/jicksta/superators 386 | * https://github.com/baweaver/xf 387 | 388 | ## Contributing 389 | 390 | * Fork the project. 391 | * Make your feature addition or bug fix. 392 | * Add tests for it. This is important so we don't break it in a future version unintentionally. 393 | * Commit, do not mess with the version or history. 394 | * Open a pull request. Bonus points for topic branches. 395 | 396 | ## Authors 397 | 398 | * [Sean Huber](https://github.com/shuber) 399 | 400 | ## License 401 | 402 | [MIT](https://github.com/lendinghome/pipe_operator/blob/master/LICENSE) - Copyright © 2018 LendingHome 403 | -------------------------------------------------------------------------------- /Rakefile: -------------------------------------------------------------------------------- 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 99 | -------------------------------------------------------------------------------- /lib/pipe_operator.rb: -------------------------------------------------------------------------------- 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 | 17 | class << self 18 | def gem 19 | @gem ||= ::Gem::Specification.load("#{root}/pipe_operator.gemspec") 20 | end 21 | 22 | def inspect(object) 23 | object.inspect 24 | rescue ::NoMethodError 25 | singleton = singleton(object) 26 | name = singleton.name || singleton.superclass.name 27 | id = "0x0000%x" % (object.__id__ << 1) 28 | "#<#{name}:#{id}>" 29 | end 30 | 31 | def root 32 | @root ||= ::Pathname.new(__dir__).join("..") 33 | end 34 | 35 | def singleton(object) 36 | (class << object; self end) 37 | rescue ::TypeError 38 | object.class 39 | end 40 | 41 | def version 42 | @version ||= gem.version.to_s 43 | end 44 | end 45 | end 46 | 47 | BasicObject.send(:include, PipeOperator) 48 | Kernel.alias_method(:pipe, :__pipe__) 49 | -------------------------------------------------------------------------------- /lib/pipe_operator/closure.rb: -------------------------------------------------------------------------------- 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 67 | -------------------------------------------------------------------------------- /lib/pipe_operator/observer.rb: -------------------------------------------------------------------------------- 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 14 | -------------------------------------------------------------------------------- /lib/pipe_operator/pipe.rb: -------------------------------------------------------------------------------- 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 | private 67 | 68 | def method_missing(method, *curry, &block) 69 | closure = Closure.new(self, method, *curry, &block) 70 | 71 | pipe = Pipe.open 72 | pipe && [*curry, block].each { |o| pipe.__pop__(o) } 73 | 74 | if pipe == self 75 | __push__(closure.__shift__) 76 | elsif pipe 77 | pipe.__push__(closure) 78 | else 79 | closure 80 | end 81 | end 82 | end 83 | end 84 | -------------------------------------------------------------------------------- /lib/pipe_operator/proxy.rb: -------------------------------------------------------------------------------- 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 59 | -------------------------------------------------------------------------------- /lib/pipe_operator/proxy_resolver.rb: -------------------------------------------------------------------------------- 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 72 | -------------------------------------------------------------------------------- /pipe_operator.gemspec: -------------------------------------------------------------------------------- 1 | Gem::Specification.new do |s| 2 | s.author = "LendingHome" 3 | s.email = "engineering@lendinghome.com" 4 | s.extra_rdoc_files = ["LICENSE"] 5 | s.files = `git ls-files 2>/dev/null`.split("\n") 6 | s.homepage = "https://github.com/lendinghome/pipe_operator" 7 | s.license = "MIT" 8 | s.name = "pipe_operator" 9 | s.required_ruby_version = ">= 2.0.0" 10 | s.summary = "Elixir/Unix style pipe operations in Ruby" 11 | s.test_files = `git ls-files -- spec/* 2>/dev/null`.split("\n") 12 | s.version = "0.0.2" 13 | 14 | s.rdoc_options = %w[ 15 | --all 16 | --charset=UTF-8 17 | --hyperlink-all 18 | --inline-source 19 | --line-numbers 20 | --main README.md 21 | ] 22 | end 23 | -------------------------------------------------------------------------------- /spec/pipe_operator_spec.rb: -------------------------------------------------------------------------------- 1 | RSpec.describe PipeOperator do 2 | describe ".gem" do 3 | it "returns a Gem::Specification" do 4 | gem = PipeOperator.gem 5 | expect(gem).to be_a(::Gem::Specification) 6 | expect(gem.name).to eq("pipe_operator") 7 | expect(gem.version).to be_a(::Gem::Version) 8 | end 9 | end 10 | 11 | describe ".root" do 12 | it "returns a Pathname to gem root" do 13 | expected = ::Pathname.new(__dir__).join("..") 14 | expect(PipeOperator.root).to match(expected) 15 | end 16 | end 17 | 18 | describe ".version" do 19 | it "returns a version string" do 20 | expected = /^\d+\.\d+\.\d+$/ 21 | expect(PipeOperator.version).to match(expected) 22 | end 23 | end 24 | 25 | describe "#pipe" do 26 | it "doesn't break existing behavior" do 27 | actual = Math.sqrt(9) 28 | expect(actual).to eq(3) 29 | end 30 | 31 | it "returns a pipe object that responds to anything" do 32 | actual = Math.pipe 33 | expect(actual).to be_a(PipeOperator::Pipe) 34 | expect { actual.anything }.not_to raise_error 35 | end 36 | 37 | it "returns a callable proc" do 38 | pipe = Math.pipe 39 | sqrt = pipe.sqrt 40 | expect(sqrt).to be_a(::Proc) 41 | expect(sqrt).to be_a(PipeOperator::Closure) 42 | 43 | actual = sqrt[9] 44 | expect(actual).to eq(3) 45 | 46 | actual = sqrt.(9) 47 | expect(actual).to eq(3) 48 | 49 | actual = sqrt.call(9) 50 | expect(actual).to eq(3) 51 | end 52 | 53 | it "casts to &block" do 54 | actual = [9].map(&Math.pipe.sqrt) 55 | expect(actual).to eq([3]) 56 | 57 | actual = [3].map(&2.pipe.send(:*)) 58 | expect(actual).to eq([6]) 59 | end 60 | 61 | it "curries arguments and blocks" do 62 | actual = "testing".pipe.sub("test").("TEST") 63 | expect(actual).to eq("TESTing") 64 | 65 | actual = ["testing"].pipe.map(&:upcase).call 66 | expect(actual).to eq(["TESTING"]) 67 | 68 | actual = ["testing"].pipe.map(&:upcase).call(&:reverse) 69 | expect(actual).to eq(["gnitset"]) 70 | 71 | actual = 2.pipe{Math.atan2(3)} 72 | expect(actual).to eq(0.982793723247329) 73 | 74 | actual = -2.pipe{abs; Math.atan2(self, 3); to_s} 75 | expect(actual).to eq("0.5880026035475675") 76 | end 77 | 78 | it "behaves like __send__ with args and no block" do 79 | sqrt = Math.pipe(:sqrt) 80 | actual = sqrt.call(16) 81 | expect(actual).to eq(4.0) 82 | 83 | sqrt = Math.pipe(:sqrt, 16) 84 | actual = sqrt.call 85 | expect(actual).to eq(4.0) 86 | 87 | expect { sqrt.call(16) }.to raise_error(::ArgumentError) 88 | end 89 | 90 | it "behaves like instance_exec with a block" do 91 | actual = "abc".pipe { reverse } 92 | expect(actual).to eq("cba") 93 | 94 | actual = "abc".pipe { reverse.upcase } 95 | expect(actual).to eq("CBA") 96 | 97 | actual = "abc".pipe { reverse; upcase } 98 | expect(actual).to eq("CBA") 99 | end 100 | 101 | it "supports calling objects on other methods" do 102 | actual = "abc".pipe { Marshal.dump; Base64.encode64 } 103 | expect(actual).to eq("BAhJIghhYmMGOgZFVA==\n") 104 | end 105 | 106 | it "supports pipe and stream expressions" do 107 | actual = "-9".pipe{to_i} 108 | expect(actual).to eq(-9) 109 | 110 | actual = "-9".pipe{to_i; abs} 111 | expect(actual).to eq(9) 112 | 113 | actual = "-9".pipe{to_i; abs; Math.sqrt} 114 | expect(actual).to eq(3) 115 | 116 | actual = "-9".pipe { to_i; abs; Math.sqrt } 117 | expect(actual).to eq(3) 118 | 119 | actual = "-9".pipe{to_i; abs; Math.sqrt; to_i; send(:*, 2)} 120 | expect(actual).to eq(6) 121 | 122 | actual = "-16".pipe{ 123 | to_i 124 | abs 125 | Math.sqrt 126 | Math.sqrt 127 | } 128 | expect(actual).to eq(2) 129 | 130 | actual = ["-16", "256"].pipe do 131 | lazy # streams 132 | map { |n| n.to_i.abs } 133 | map { |n| n * 2 } 134 | end 135 | expect(actual).to be_an(Enumerator::Lazy) 136 | expect(actual.to_a).to eq([32, 512]) 137 | end 138 | 139 | it "resolves recursive pipes" do 140 | actual = ["-16", "256"].pipe do 141 | lazy 142 | map { |n| n.to_i.abs } 143 | map { |n| n * 2 } 144 | map(&Math.sqrt) 145 | map(&Math.sqrt) 146 | reduce(&:+) 147 | Math.sqrt 148 | ceil 149 | end 150 | expect(actual).to eq(3) 151 | end 152 | 153 | it "resolves pipe chain" do 154 | pipe = Math.pipe.sqrt.to_i.to_s 155 | actual = pipe.call(256) 156 | expect(actual).to eq("16") 157 | 158 | actual = 64.pipe{Math.sqrt.to_i.to_s} 159 | expect(actual).to eq("8") 160 | 161 | actual = [64].map(&Math.pipe.sqrt.to_i.to_s) 162 | expect(actual).to eq(["8"]) 163 | 164 | actual = [64, 256].map(&Math.pipe.sqrt.to_i.to_s) 165 | expect(actual).to eq(["8", "16"]) 166 | end 167 | 168 | it "proxies pipe arguments", :pending do 169 | class Markdown 170 | def format(string) 171 | string.upcase 172 | end 173 | end 174 | 175 | actual = Markdown.new.pipe.format.call("test") 176 | expect(actual).to eq("TEST") 177 | 178 | actual = "test".pipe(Markdown.new, &:format) 179 | expect(actual).to eq("TEST") 180 | end 181 | 182 | it "observes method changes" do 183 | methods = Math.methods(false).sort 184 | expect(methods).not_to be_empty 185 | 186 | proxy = PipeOperator::ProxyResolver.new(Math).proxy 187 | expect(proxy.definitions).to eq(methods) 188 | expect(proxy.definitions).not_to include(:test) 189 | 190 | def Math.test; end 191 | expect(proxy.definitions).to include(:test) 192 | 193 | Math.singleton_class.remove_method(:test) 194 | expect(proxy.definitions).not_to include(:test) 195 | 196 | expect{ proxy.undefine(:invalid) }.not_to raise_error 197 | end 198 | 199 | it "varies Pipe#inspect based on the object" do 200 | basic = ::BasicObject.new 201 | def basic.hash; 0 end 202 | 203 | expected = { 204 | ::BasicObject => "#", 205 | ::Class => "#", 206 | ::Class.new => /#>/, 207 | ::Math => "#", 208 | 123 => "#", 209 | true => "#", 210 | basic => /#>/, 211 | } 212 | 213 | expected.each do |object, inspect| 214 | pipe = object.__pipe__ 215 | matcher = Regexp === inspect ? match(inspect) : eq(inspect) 216 | expect(pipe.inspect).to matcher 217 | end 218 | end 219 | 220 | it "varies ProxyResolver#inspect based on the object" do 221 | basic = ::BasicObject.new 222 | def basic.hash; 0 end 223 | 224 | expected = { 225 | ::BasicObject => "#", 226 | ::Class => "#", 227 | ::Class.new => /#>/, 228 | ::Math => "#", 229 | 123 => "#>", 230 | true => "#>", 231 | basic => /#>/, 232 | } 233 | 234 | expected.each do |object, inspect| 235 | proxy = PipeOperator::ProxyResolver.new(object).proxy 236 | matcher = Regexp === inspect ? match(inspect) : eq(inspect) 237 | expect(proxy.inspect).to matcher 238 | end 239 | end 240 | end 241 | end 242 | -------------------------------------------------------------------------------- /spec/spec_helper.rb: -------------------------------------------------------------------------------- 1 | require "simplecov" 2 | SimpleCov.start { add_filter("/vendor/bundle/") } 3 | 4 | require "base64" 5 | require "json" 6 | require "pry" 7 | require "pry-byebug" 8 | 9 | ENV["PIPE_OPERATOR_FROZEN"] ||= "1" 10 | require_relative "../lib/pipe_operator" 11 | 12 | RSpec.configure do |config| 13 | config.filter_run :focus 14 | config.raise_errors_for_deprecations! 15 | config.run_all_when_everything_filtered = true 16 | end 17 | --------------------------------------------------------------------------------