waterslide 0.1.0

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.
Files changed (8) hide show
  1. checksums.yaml +7 -0
  2. data/.gitignore +35 -0
  3. data/LICENSE +22 -0
  4. data/README.md +170 -0
  5. data/lib/waterslide.rb +65 -0
  6. data/test.rb +178 -0
  7. data/waterslide.gemspec +18 -0
  8. metadata +50 -0
checksums.yaml ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA1:
3
+ metadata.gz: 301ba4b66544ec9a3c9291e2f0ca8b993881a009
4
+ data.tar.gz: 3267b0740f98719e6f791dccbc5b177eb0862813
5
+ SHA512:
6
+ metadata.gz: 2529d565230da7c62d34593935581180b2e1c0499ee851660ac1fe66461e33d5b1fc3fd91a136131a85d617307fcaf9939934160159bddbdcadb5c42225e2a4e
7
+ data.tar.gz: c2c0a1baad887e297c0d8ba90680e016e0fa116209b559d760cef3a99167d7a9f6e8f47f68b39c170359b7154e6d670eea6c647ecb903deb5966406c4abc1c5a
data/.gitignore ADDED
@@ -0,0 +1,35 @@
1
+ *.gem
2
+ *.rbc
3
+ /.config
4
+ /coverage/
5
+ /InstalledFiles
6
+ /pkg/
7
+ /spec/reports/
8
+ /test/tmp/
9
+ /test/version_tmp/
10
+ /tmp/
11
+
12
+ ## Specific to RubyMotion:
13
+ .dat*
14
+ .repl_history
15
+ build/
16
+
17
+ ## Documentation cache and generated files:
18
+ /.yardoc/
19
+ /_yardoc/
20
+ /doc/
21
+ /rdoc/
22
+
23
+ ## Environment normalisation:
24
+ /.bundle/
25
+ /vendor/bundle
26
+ /lib/bundler/man/
27
+
28
+ # for a library or gem, you might want to ignore these files since the code is
29
+ # intended to run in multiple environments; otherwise, check them in:
30
+ # Gemfile.lock
31
+ # .ruby-version
32
+ # .ruby-gemset
33
+
34
+ # unless supporting rvm < 1.11.0 or doing something fancy, ignore this:
35
+ .rvmrc
data/LICENSE ADDED
@@ -0,0 +1,22 @@
1
+ The MIT License (MIT)
2
+
3
+ Copyright (c) 2015 Ben Christel
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
22
+
data/README.md ADDED
@@ -0,0 +1,170 @@
1
+ # waterslide
2
+
3
+ Unix-style pipes for Ruby programs
4
+
5
+ ## TL;DR
6
+
7
+ ```ruby
8
+ class FacebookFriendsController < ApplicationController
9
+ def index
10
+ render json:
11
+ FacebookFriends.new(current_user) >>
12
+ DeserializeFacebookUsers >>
13
+ MergeWithAttributesFromDatabase >>
14
+ RemoveRecordsNotInDatabase >>
15
+ Serialize
16
+ end
17
+ end
18
+ ```
19
+
20
+ ## Wat
21
+
22
+ A common problem in programming is the need to perform a series of transformations on data. Ruby enumerables have lots of nice functional-style methods (`map`, `reduce`) to make this easy, but in some cases this approach breaks down.
23
+
24
+ Let's say you're building a social app where users log in with Facebook. You want to have a page where users can see which of their Facebook friends are using your site.
25
+
26
+ Doing this requires calling the Facebook API to get the current user's friends, merging the data from Facebook with the user data in your own database, filtering out the Facebook users who don't have accounts on your site, and serializing the remaining users to HTML or JSON so you can render them on your page.
27
+
28
+ The usual solution might look something like this:
29
+
30
+ ```ruby
31
+ Facebook.friends_of(current_user)
32
+ .map { |friend_datum| FacebookUserDeserializer.deserialize(friend_datum) }
33
+ .map { |user| user.merge_attributes User.find_by_facebook_id(user.facebook_id) }
34
+ .reject { |user| user.id.nil? }
35
+ .map { |user| UserSerializer.serialize(user) }
36
+ ```
37
+
38
+ Later, you find that the page takes a long time to load for users with many facebook friends, and you isolate the problem to the O(n) `User.find_by_facebook_id` calls, most of which don't actually find a user. Fortunately, that's not hard to fix.
39
+
40
+ ```ruby
41
+ facebook_friends = Facebook.friends_of(current_user).map do |friend_datum|
42
+ FacebookUserDeserializer.deserialize(friend_datum)
43
+ end
44
+ facebook_ids = facebook_friends.map(&:facebook_id)
45
+ facebook_friends_from_database = User.where(facebook_id: facebook_ids)
46
+ facebook_friends.map! do |friend|
47
+ db_record = facebook_friends_from_database.find do |db_record|
48
+ db_record.facebook_id == friend.facebook_id
49
+ end
50
+ friend.merge_attributes db_record
51
+ end
52
+ facebook_friends.map do |user|
53
+ UserSerializer.serialize(user)
54
+ end
55
+ ```
56
+
57
+ In making the code more efficient, its elegance has been destroyed. Now one of the `map` blocks is dependent on an invariant - the users pulled from the database - and that dependency makes the code harder to read and harder to refactor.
58
+
59
+ With Waterslide, the various data transformations can easily be broken into their own classes.
60
+
61
+ ```ruby
62
+ class FacebookFriends
63
+ include Waterslide::Pipe
64
+
65
+ def initialize(current_user)
66
+ @current_user = current_user
67
+ end
68
+
69
+ def each(&block)
70
+ @friends ||= Facebook.friends_of(@current_user)
71
+ @friends.each(&block)
72
+ end
73
+ end
74
+
75
+ class DeserializeFacebookUsers
76
+ include Waterslide::Pipe
77
+
78
+ def pipe_one(user_json)
79
+ yield User.new#( ... )
80
+ end
81
+ end
82
+
83
+ class MergeWithAttributesFromDatabase
84
+ include Waterslide::Pipe
85
+
86
+ def pipe_one(user)
87
+ record = database_records.find do |record|
88
+ record.facebook_id == user.facebook_id
89
+ end
90
+
91
+ yield user.merge_attributes record
92
+ end
93
+
94
+ def database_records
95
+ @records ||= User.where(facebook_id: incoming.map(&:facebook_id)).to_a
96
+ end
97
+ end
98
+
99
+ class RemoveRecordsNotInDatabase
100
+ include Waterslide::Pipe
101
+
102
+ def pipe_one(record)
103
+ yield record if record.id
104
+ end
105
+ end
106
+
107
+ class Serialize
108
+ include Waterslide::Pipe
109
+
110
+ def as_json
111
+ map(&:as_json)
112
+ end
113
+ end
114
+
115
+ # ...
116
+
117
+ FacebookFriends.new(current_user) >>
118
+ DeserializeFacebookUsers >>
119
+ MergeWithAttributesFromDatabase >>
120
+ RemoveRecordsNotInDatabase >>
121
+ SerializeUsers
122
+ ```
123
+
124
+ There's obviously a lot more lines of code in the new version, but the sequence of transformations reads naturally, and, perhaps more importantly, every step can now be unit-tested individually.
125
+
126
+ ## Installation
127
+
128
+ Add this line to your application's Gemfile:
129
+
130
+ ```ruby
131
+ gem 'waterslide'
132
+ ```
133
+
134
+ And then execute:
135
+
136
+ $ bundle
137
+
138
+ Or install it yourself as:
139
+
140
+ $ gem install waterslide
141
+
142
+ ## Usage
143
+
144
+ TODO: Write usage instructions here
145
+
146
+ ## Serving Suggestions
147
+
148
+ If you like syntactic sugar on your cerealizables, you may want to monkey-patch Array with the Waterslide right-shift operator override. That will let you do stuff like this:
149
+
150
+ ```ruby
151
+ [1, 2, 3] >> MultiplyByTwo # => [2, 4, 6]
152
+ ```
153
+
154
+ Here's how to do the monkey-patch:
155
+
156
+ ```ruby
157
+ class Array
158
+ include Waterslide::RightShiftOverride
159
+ end
160
+ ```
161
+
162
+ You should probably only do this if everyone on your team is on board with Waterslide and knows how to use it; otherwise, they'll have a hell of time deciphering your code.
163
+
164
+ ## Contributing
165
+
166
+ 1. Fork it ( https://github.com/benchristel/waterslide/fork )
167
+ 2. Create your feature branch (`git checkout -b my-new-feature`)
168
+ 3. Commit your changes (`git commit -am 'Add some feature'`)
169
+ 4. Push to the branch (`git push origin my-new-feature`)
170
+ 5. Create a new Pull Request
data/lib/waterslide.rb ADDED
@@ -0,0 +1,65 @@
1
+ module Waterslide
2
+ module RightShiftOverride
3
+ def >> (pipe)
4
+ instantiated(pipe).receive_from(self)
5
+ end
6
+
7
+ private
8
+ def instantiated(pipe)
9
+ pipe.is_a?(Class) ? pipe.new : pipe
10
+ end
11
+ end
12
+
13
+ module Pipe
14
+ def self.included(base)
15
+ base.class_eval do
16
+ include Enumerable
17
+ include RightShiftOverride
18
+ end
19
+ end
20
+
21
+ def self.[] (things)
22
+ things = [things] unless things.respond_to? :each
23
+ NoOp.new.receive_from(things)
24
+ end
25
+
26
+ def receive_from(enumerable)
27
+ @incoming = enumerable
28
+ self
29
+ end
30
+
31
+ def each
32
+ incoming.each do |one|
33
+ pipe_one(one) { |out| yield out }
34
+ end
35
+ end
36
+
37
+ def take
38
+ each { |one| return one }
39
+ return nil # if nothing was yielded
40
+ end
41
+
42
+ def all
43
+ all = []
44
+ each { |one| all << one }
45
+ all
46
+ end
47
+
48
+ private
49
+
50
+ def incoming
51
+ # including classes may override this to do processing on the incoming
52
+ # enumerable as as whole - for instance, to sort it.
53
+ @incoming
54
+ end
55
+
56
+ def pipe_one(thing)
57
+ # identity function by default; including classes should override this
58
+ yield thing
59
+ end
60
+ end
61
+
62
+ class NoOp
63
+ include Pipe
64
+ end
65
+ end
data/test.rb ADDED
@@ -0,0 +1,178 @@
1
+ gem 'minitest'
2
+ require 'minitest/autorun'
3
+ require_relative 'lib/waterslide'
4
+
5
+ include Waterslide
6
+
7
+ class AddOne
8
+ include Pipe
9
+
10
+ def pipe_one(thing)
11
+ yield thing + 1
12
+ end
13
+ end
14
+
15
+ class Add
16
+ include Pipe
17
+
18
+ def initialize(n)
19
+ @increment = n
20
+ end
21
+
22
+ def pipe_one(thing)
23
+ yield thing + @increment
24
+ end
25
+ end
26
+
27
+ def Add(*args)
28
+ Add.new(*args)
29
+ end
30
+
31
+ class TestWaterslide < MiniTest::Test
32
+ def test_piping_a_scalar_through_no_op
33
+ assert_equal 1, (Pipe[1] >> NoOp).first
34
+ end
35
+
36
+ def test_piping_a_scalar_through_multiple_no_ops
37
+ assert_equal 1, (Pipe[1] >> NoOp >> NoOp).first
38
+ end
39
+
40
+ def test_piping_a_scalar_through_add_one
41
+ assert_equal 2, (Pipe[1] >> AddOne).first
42
+ end
43
+
44
+ def test_piping_a_scalar_through_multiple_add_ones
45
+ assert_equal 3, (Pipe[1] >> AddOne >> AddOne).first
46
+ end
47
+
48
+ def test_piping_an_array_through_no_op
49
+ assert_equal [1,2,3], (Pipe[[1,2,3]] >> NoOp).all
50
+ end
51
+
52
+ def test_piping_an_array_through_add_one
53
+ assert_equal [2,3,4], (Pipe[[1,2,3]] >> AddOne).all
54
+ end
55
+
56
+ def test_piping_an_array_through_multiple_add_ones
57
+ assert_equal [3,4,5], (Pipe[[1,2,3]] >> AddOne >> AddOne).all
58
+ end
59
+
60
+ def test_piping_an_array_through_add
61
+ assert_equal [4,5,6], (Pipe[[1,2,3]] >> Add(3)).all
62
+ end
63
+
64
+ def test_that_pipes_are_enumerables
65
+ assert (Pipe[[1,2,3]] >> Add(3)).include? 4
66
+ assert_equal 3, (Pipe[[1,2,3]] >> Add(3)).count
67
+ end
68
+
69
+
70
+
71
+ class Duplicate
72
+ include Pipe
73
+
74
+ def pipe_one(thing)
75
+ yield thing
76
+ yield thing
77
+ end
78
+ end
79
+
80
+ def test_duplicate
81
+ assert_equal [1,1,2,2,3,3], (Pipe[[1,2,3]] >> Duplicate).all
82
+ end
83
+
84
+
85
+
86
+ class OnlyEvens
87
+ include Pipe
88
+
89
+ def pipe_one(n)
90
+ yield n if n % 2 == 0
91
+ end
92
+ end
93
+
94
+ def test_piping_an_array_through_a_filter
95
+ assert_equal [2,4,6], (Pipe[[1,2,3,4,5,6]] >> OnlyEvens).all
96
+ end
97
+
98
+
99
+
100
+ class Sort
101
+ include Pipe
102
+
103
+ def incoming
104
+ super.sort
105
+ end
106
+ end
107
+
108
+ def test_sorting_with_overridden_incoming
109
+ assert_equal [1,2,3,4,5], (Pipe[[4,1,5,3,2]] >> Sort).all
110
+ end
111
+
112
+
113
+
114
+ class Sort::Descending < Sort
115
+ def incoming
116
+ super.reverse
117
+ end
118
+ end
119
+
120
+ def test_subclasses_of_pipes
121
+ assert_equal [5,4,3,2,1], (Pipe[[4,1,5,3,2]] >> Sort::Descending).all
122
+ end
123
+
124
+
125
+
126
+ class AboveAverage
127
+ include Pipe
128
+
129
+ def pipe_one(thing)
130
+ yield thing if thing > average
131
+ end
132
+
133
+ def average
134
+ @average ||= incoming.reduce(:+) / incoming.count
135
+ end
136
+ end
137
+
138
+ def test_incoming_with_above_average
139
+ assert_equal [4,5], (Pipe[[1,2,3,4,5]] >> AboveAverage).all
140
+ end
141
+
142
+
143
+
144
+ class InfiniteJest
145
+ include Pipe
146
+
147
+ def each
148
+ n = 0
149
+ while(n < 6)
150
+ yield 'ha'
151
+ n += 1
152
+ end
153
+ raise 'oh no you are dead'
154
+ end
155
+ end
156
+
157
+ def test_that_enumeration_is_lazy_when_possible
158
+ haha = (InfiniteJest.new >> NoOp).first(5)
159
+ assert_equal ["ha"]*5, haha
160
+ end
161
+
162
+ def test_that_enumeration_is_not_lazy_when_impossible
163
+ haha = (InfiniteJest.new >> Sort).first(5) rescue 'got to infinity'
164
+ assert_equal 'got to infinity', haha
165
+ end
166
+
167
+
168
+
169
+ class MagicArray < Array
170
+ include Waterslide::RightShiftOverride
171
+ end
172
+
173
+ def test_right_shift_operator_override
174
+ array = MagicArray.new
175
+ array << 1 << 2 << 3
176
+ assert (array >> Add(2)).map(&:to_i) == [3, 4, 5]
177
+ end
178
+ end
@@ -0,0 +1,18 @@
1
+ lib = File.expand_path('../lib', __FILE__)
2
+ $LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
3
+
4
+ Gem::Specification.new do |s|
5
+ version = "0.1.0"
6
+
7
+ s.name = "waterslide"
8
+ s.version = version
9
+ s.platform = Gem::Platform::RUBY
10
+ s.license = "MIT"
11
+ s.authors = ["Ben Christel"]
12
+ s.homepage = "http://github.com/benchristel/waterslide"
13
+ s.summary = "waterslide-#{version}"
14
+ s.description = "Unix-style pipes in Ruby"
15
+ s.files = `git ls-files`.split("\n")
16
+ s.test_files = s.files.grep(/^test/)
17
+ s.require_paths = ["lib"]
18
+ end
metadata ADDED
@@ -0,0 +1,50 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: waterslide
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.1.0
5
+ platform: ruby
6
+ authors:
7
+ - Ben Christel
8
+ autorequire:
9
+ bindir: bin
10
+ cert_chain: []
11
+ date: 2015-04-19 00:00:00.000000000 Z
12
+ dependencies: []
13
+ description: Unix-style pipes in Ruby
14
+ email:
15
+ executables: []
16
+ extensions: []
17
+ extra_rdoc_files: []
18
+ files:
19
+ - ".gitignore"
20
+ - LICENSE
21
+ - README.md
22
+ - lib/waterslide.rb
23
+ - test.rb
24
+ - waterslide.gemspec
25
+ homepage: http://github.com/benchristel/waterslide
26
+ licenses:
27
+ - MIT
28
+ metadata: {}
29
+ post_install_message:
30
+ rdoc_options: []
31
+ require_paths:
32
+ - lib
33
+ required_ruby_version: !ruby/object:Gem::Requirement
34
+ requirements:
35
+ - - ">="
36
+ - !ruby/object:Gem::Version
37
+ version: '0'
38
+ required_rubygems_version: !ruby/object:Gem::Requirement
39
+ requirements:
40
+ - - ">="
41
+ - !ruby/object:Gem::Version
42
+ version: '0'
43
+ requirements: []
44
+ rubyforge_project:
45
+ rubygems_version: 2.4.6
46
+ signing_key:
47
+ specification_version: 4
48
+ summary: waterslide-0.1.0
49
+ test_files:
50
+ - test.rb