pipetree 0.0.1 → 0.0.2

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA1:
3
- metadata.gz: c4c06f28009c3c0336a76d69b09ca4ecc1d6821a
4
- data.tar.gz: 949fbc15906d193bcabbbf84377d1befc00fdae1
3
+ metadata.gz: 372090919caec356b0dccdfb2c3dc80ff18e565c
4
+ data.tar.gz: 4c34b1b76303b7df53db05605b2037f2f559b7f9
5
5
  SHA512:
6
- metadata.gz: 06ce0641d861248bc44d97b62182327b6133ac57c8cd22223b26c6260436e8b082bae8447eef4c08b3b4c67968a838f74dc87b8dfb44ac8496f6e8bc90b09662
7
- data.tar.gz: 224c93d3304b71dfce2bb828d63562336cc6d050ea99b5d9828eb2574908d8f1e76589edb7545590321e47a42740014deaa7d2719aeb8ddf0d85f6a5481ff176
6
+ metadata.gz: b29ec2b9ea9f2503ebf53336ae433fdf54c57eb1d9f0ffb40750ec4d093608978fec321453e357e88fd939359921535eb76ea4fa4f9b1aedc030364cab215723
7
+ data.tar.gz: 9cc798a6dd1d4d055d627e21533e4212060afb55583dd5766ce76449d1aa54714c18800610281d7de00015184aa5bb8f91d01e564cc9c5916eac018a588f1ce9
data/CHANGES.md ADDED
@@ -0,0 +1,3 @@
1
+ # 0.0.2
2
+
3
+ * Add `Pipetree::Flow`.
data/Gemfile ADDED
@@ -0,0 +1,8 @@
1
+ source "https://rubygems.org"
2
+
3
+ gemspec
4
+
5
+ gem "benchmark-ips"
6
+
7
+ gem "dry-monads"
8
+ gem "minitest-line"
data/README.md ADDED
@@ -0,0 +1,23 @@
1
+ Example use: trailblazer.to/gems/operation/2.0/pipetree.html
2
+
3
+
4
+ Executing a pipetree generated from declarative configuration.
5
+
6
+ This reduces conditionals at run-time,
7
+ Replaces clumsy module includes to inject or override features (in the correct order) with explicit, (visual!) pipelines
8
+ Speeds up as only the pipelines need to be run without too much decider code.
9
+
10
+
11
+ First used in the Representable gem.
12
+
13
+
14
+
15
+ Instead of implementing the perfect API where users can override methods, call `super` or inject their logic, let them construct their own workflow.
16
+
17
+ This is way less tedious than climbing through `super` calls and callstacks.
18
+
19
+
20
+ ## TODO
21
+
22
+ * Catch exceptions and show where in the pipe they were raised.
23
+ * Statically compile pipetrees into Ruby methods to make it lightning fast (paid gem?)
data/Rakefile ADDED
@@ -0,0 +1,10 @@
1
+ require 'bundler/setup'
2
+ require 'rake/testtask'
3
+
4
+ task :default => :test
5
+
6
+ Rake::TestTask.new(:test) do |test|
7
+ test.libs << 'test'
8
+ test.test_files = FileList['test/*_test.rb']
9
+ test.verbose = true
10
+ end
@@ -0,0 +1,21 @@
1
+ require "pipetree/inspect"
2
+
3
+ module Pipetree::Flow::Inspect
4
+ include ::Pipetree::Inspect
5
+
6
+ Proc = Struct.new(:proc, :operator)
7
+
8
+ def inspect_for(step)
9
+ debug = @debug[step]
10
+ [super(debug.proc), debug.operator]
11
+ end
12
+
13
+ def inspect_line(names)
14
+ string = names.collect { |i, name| "#{name.last}#{name.first}" }.join(",")
15
+ "[#{string}]"
16
+ end
17
+
18
+ def inspect_row(index, name)
19
+ "#{index} #{name.last}#{name.first}"
20
+ end
21
+ end
@@ -0,0 +1,104 @@
1
+ class Pipetree::Flow < Array # yes, we could inherit, and so on.
2
+ require "pipetree/flow/inspect"
3
+ include Inspect
4
+
5
+ module Operators
6
+ # Optimize the most common steps with Stay/And objects that are faster than procs.
7
+ def <(proc, options=nil)
8
+ _insert On.new(Left, Stay.new(proc)), options, proc, "<"
9
+ end
10
+
11
+ # OnRight-> ? Right, input : Left, input
12
+ def &(proc, options=nil)
13
+ _insert On.new(Right, And.new(proc)), options, proc, "&"
14
+ end
15
+
16
+ # TODO: test me.
17
+ def >(proc, options=nil)
18
+ _insert On.new(Right, Stay.new(proc)), options, proc, ">"
19
+ end
20
+
21
+ def >>(proc, options=nil)
22
+ _insert On.new(Right,
23
+ ->(last, input, options) { [Right, proc.(input, options)] } ), options, proc, ">>"
24
+ end
25
+
26
+ def %(proc, options=nil)
27
+ # no condition is needed, and we want to stay on the same track, too.
28
+ _insert Stay.new(proc), options, proc, "%"
29
+ end
30
+
31
+ # :private:
32
+ # proc is the original step proc, e.g. Validate.
33
+ def _insert(step, options, proc, operator)
34
+ options ||= { append: true } # DISCUSS: needed?
35
+
36
+ insert!(step, options).tap do
37
+ @debug ||= {}
38
+ @debug[step] = Inspect::Proc.new(proc, operator)
39
+ end
40
+ end
41
+ end
42
+ include Operators
43
+
44
+ # Actual implementation of Pipetree:Flow. Yes, it's that simple!
45
+ def call(input, options)
46
+ input = [Right, input]
47
+
48
+ inject(input) do |memooo, step|
49
+ last, memo = memooo
50
+ step.call(last, memo, options)
51
+ end
52
+ end
53
+
54
+ def index(step) # @debug maps the original user's step proc to the On instance (or any kind of wrapper proc).
55
+ @debug.find { |on, inspect_proc| inspect_proc.proc == step and return super(on) }
56
+ end
57
+
58
+ # Directions emitted by steps.
59
+ Left = Class.new
60
+ Right = Class.new
61
+
62
+ # Incoming direction must be Left/Right.
63
+ class On
64
+ def initialize(direction, proc)
65
+ @direction, @proc = direction, proc
66
+ end
67
+
68
+ def call(last, input, options)
69
+ return [last, input] unless last == @direction # return unless incoming direction is Right (or Left).
70
+ @proc.(last, input, options)
71
+ end
72
+ end
73
+
74
+ # Call step proc and return (Right || Left).
75
+ class And
76
+ def initialize(proc)
77
+ @proc = proc
78
+ end
79
+
80
+ def call(last, input, options)
81
+ @proc.(input, options) ? [Right, input] : [Left, input]
82
+ end
83
+ end
84
+
85
+ # Call step proc and return incoming last step.
86
+ class Stay < And
87
+ def call(last, input, options)
88
+ @proc.(input, options)
89
+ [last, input] # simply pass through the current direction: either [Left, input] or [Right, input].
90
+ end
91
+ end
92
+
93
+
94
+
95
+
96
+
97
+ require "pipetree/insert"
98
+ module Macros
99
+ def insert!(new_function, options)
100
+ Pipetree::Insert.(self, new_function, options)
101
+ end
102
+ end
103
+ include Macros # FIXME: we shouldn't expose #insert!
104
+ end
@@ -0,0 +1,61 @@
1
+ module Pipetree::Function
2
+ class Insert
3
+ def call(arr, func, options)
4
+ # arr = arr.dup
5
+ return delete!(arr, func) if options[:delete]
6
+ return replace!(arr, options[:replace], func) if options[:replace]
7
+ return before!(arr, options[:before], func) if options[:before]
8
+ return after!(arr, options[:after], func) if options[:after]
9
+ return append!(arr, options[:append], func) if options[:append]
10
+ return prepend!(arr, options[:prepend], func) if options[:prepend]
11
+
12
+ raise "[Pipetree] Unknown command #{options.inspect}" # TODO: test.
13
+ # arr
14
+ end
15
+
16
+ private
17
+ def replace!(arr, old_func, new_func)
18
+ arr.each_with_index { |func, index|
19
+ if func.is_a?(Collect)
20
+ arr[index] = Collect[*Pipeline::Insert.(func, new_func, replace: old_func)]
21
+ end
22
+
23
+ arr[index] = new_func if func==old_func
24
+ }
25
+ end
26
+
27
+ def delete!(arr, removed_func)
28
+ arr.delete(removed_func)
29
+
30
+ # TODO: make nice.
31
+ arr.each_with_index { |func, index|
32
+ if func.is_a?(Collect)
33
+ arr[index] = Collect[*Pipeline::Insert.(func, removed_func, delete: true)]
34
+ end
35
+ }
36
+ end
37
+
38
+ # TODO: not nested.
39
+ def before!(arr, old_func, new_func)
40
+ index = arr.index(old_func)
41
+ arr.insert(index, new_func)
42
+ end
43
+
44
+ def after!(arr, old_func, new_func)
45
+ index = arr.index(old_func)+1
46
+ arr.insert(index, new_func)
47
+ end
48
+
49
+ def append!(arr, old_func, new_func)
50
+ arr << (new_func)
51
+ end
52
+
53
+ def prepend!(arr, old_func, new_func)
54
+ arr.unshift(new_func)
55
+ end
56
+ end
57
+ end
58
+
59
+ Pipetree::Insert = Pipetree::Function::Insert.new
60
+
61
+ #FIXME: all methods write to original array.
@@ -0,0 +1,33 @@
1
+ module Pipetree::Inspect
2
+ # TODO: implement for nested
3
+ # TODO: remove in Representable::Debug.
4
+ def inspect(options={ style: :line })
5
+ names = each_with_index.collect do |func, i|
6
+ [i, inspect_for(func)]
7
+ end
8
+
9
+ return inspect_line(names) if options[:style] == :line
10
+
11
+ string = names.collect do |i, name|
12
+ index = sprintf("%2d", i)
13
+ inspect_row(index, name)
14
+ end.join("\n")
15
+ # name = sprintf("%-60.300s", name) # no idea what i'm doing here.
16
+ # "#{index}) #{name} #{func.source_location.join(":")}"
17
+ "\n#{string}"
18
+ end
19
+
20
+ # open source file to retrieve the constant name.
21
+ def inspect_for(func)
22
+ File.readlines(func.source_location[0])[func.source_location[1]-1].match(/^\s+([\w\:]+)/)[1]
23
+ end
24
+
25
+ def inspect_line(names)
26
+ string = names.collect { |i, name| "#{name}" }.join("|>")
27
+ "[#{string}]"
28
+ end
29
+
30
+ def inspect_row(index, name)
31
+ "#{index}|>#{name}"
32
+ end
33
+ end
@@ -1,3 +1,3 @@
1
1
  module Pipetree
2
- VERSION = "0.0.1"
2
+ VERSION = "0.0.2"
3
3
  end
data/lib/pipetree.rb CHANGED
@@ -1,3 +1,57 @@
1
- module Pipetree
1
+ class Pipetree < Array
2
+ VERSION = "0.0.1"
2
3
 
4
+ # Allows to implement a pipeline of filters where a value gets passed in and the result gets
5
+ # passed to the next callable object.
6
+ Stop = Class.new
7
+
8
+ # options is mutuable.
9
+ # we have a fixed set of arguments here, since array splat significantly slows this down, as in
10
+ # call(input, *options)
11
+ def call(input, options)
12
+ inject(input) do |memo, step|
13
+ res = evaluate(step, memo, options)
14
+ return(Stop) if Stop == res
15
+ res
16
+ end
17
+ end
18
+
19
+ private
20
+ def evaluate(step, input, options)
21
+ step.call(input, options)
22
+ end
23
+
24
+ require "pipetree/inspect"
25
+ include Inspect
26
+
27
+ module Macros
28
+ def insert!(new_function, options)
29
+ Pipetree::Insert.(self, new_function, options)
30
+ end
31
+ end
32
+ require "pipetree/insert"
33
+ include Macros
34
+
35
+ # Collect applies a pipeline to each element of input.
36
+ class Collect < self
37
+ # when stop, the element is skipped. (should that be Skip then?)
38
+ def call(input, options)
39
+ arr = []
40
+ input.each_with_index do |item_fragment, i|
41
+ result = super(item_fragment, options.merge(index: i)) # DISCUSS: NO :fragment set.
42
+ Stop == result ? next : arr << result
43
+ end
44
+ arr
45
+ end
46
+
47
+ # DISCUSS: will this make it into the final version?
48
+ class Hash < self
49
+ def call(input, options)
50
+ {}.tap do |hsh|
51
+ input.each { |key, item_fragment|
52
+ hsh[key] = super(item_fragment, options) }# DISCUSS: NO :fragment set.
53
+ end
54
+ end
55
+ end
56
+ end
3
57
  end
@@ -0,0 +1,44 @@
1
+ require "test_helper"
2
+
3
+ class AlteringTest < Minitest::Spec
4
+ A = ->(*) { }
5
+ B = ->(*) { }
6
+ C = ->(*) { }
7
+
8
+ # constructor.
9
+ it do
10
+ pipe = ::Pipetree[A, B]
11
+ pipe.inspect.must_equal %{[A|>B]}
12
+ end
13
+
14
+ it { Pipetree[].insert(0, B).inspect.must_equal %{[B]} }
15
+ it { Pipetree[].unshift(B).inspect.must_equal %{[B]} }
16
+ it { Pipetree[].unshift(B, A).inspect.must_equal %{[B|>A]} }
17
+
18
+ it { Pipetree[A,B].insert!(C, before: A).inspect.must_equal %{[C|>A|>B]} }
19
+ it { Pipetree[A,B].insert!(C, before: B).inspect.must_equal %{[A|>C|>B]} }
20
+
21
+ it { Pipetree[A,B].insert!(C, after: A).inspect.must_equal %{[A|>C|>B]} }
22
+ it { Pipetree[A,B].insert!(C, after: B).inspect.must_equal %{[A|>B|>C]} }
23
+
24
+ it { Pipetree[A,B].insert!(C, append: true).inspect.must_equal %{[A|>B|>C]} }
25
+ it { Pipetree[].insert!(C, append: true).inspect.must_equal %{[C]} }
26
+
27
+ it { Pipetree[A,B].insert!(C, prepend: true).inspect.must_equal %{[C|>A|>B]} }
28
+ it { Pipetree[].insert!(C, prepend: true).inspect.must_equal %{[C]} }
29
+ end
30
+
31
+ class FlowInsertTest < Minitest::Spec
32
+ A = ->{ }
33
+ B = ->{ }
34
+ C = ->{ }
35
+
36
+ it { pipe = Pipetree::Flow[].>(A).>(B).inspect.must_equal %{[>A,>B]} }
37
+ it { pipe = Pipetree::Flow[].>(A).>(B, before: A).inspect.must_equal %{[>B,>A]} }
38
+ it { pipe = Pipetree::Flow[].>(A).>(B).>(C, after: A).inspect.must_equal %{[>A,>C,>B]} }
39
+ it { pipe = Pipetree::Flow[].>(A).>(C, append: true).inspect.must_equal %{[>A,>C]} }
40
+ it { pipe = Pipetree::Flow[].>(A).>(C, prepend: true).inspect.must_equal %{[>C,>A]} }
41
+ # it { pipe = Pipetree::Flow[].>(A).>(C, replace: A).inspect.must_equal %{[>C]} }
42
+
43
+ # FIXME: add :delete and :replace.
44
+ end
@@ -0,0 +1,180 @@
1
+ require "test_helper"
2
+
3
+
4
+ require "dry-monads"
5
+
6
+ class EitherCalculator
7
+ include Dry::Monads::Either::Mixin
8
+
9
+ attr_accessor :input
10
+
11
+ def calculate(input)
12
+ Right(input).bind do |value|
13
+ # puts "|>DESERIALIZATION"
14
+ value.nil? ? Left(value) : Right(value)
15
+ end.bind do |value|
16
+ # puts "|>VALIDATION"
17
+ value[:ok?] ? Right(value) : Left(value)
18
+ end.bind do |value|
19
+ # puts "|>PERSISTENCE"
20
+ value[:persist?] ? Right(value) : Left(value)
21
+ end.or do |value|
22
+ # puts "|| INVALID=CALLBACK"
23
+ Left(value)
24
+ end.bind do |value|
25
+ # puts "|> OK=CALLBACK"
26
+ Right(value)
27
+ end.or do |value|
28
+ # puts "|| SECOND-INVALID=CALLBACK"
29
+ Left(value)
30
+ end.or do |value|
31
+ # puts "|| FIXING IT=CALLBACK"
32
+ Right(value)
33
+ end
34
+ # .bind do |value|
35
+ # puts "|> WHATEVER HAPPENED=CALLBACK"
36
+ # Right(value)
37
+ # end
38
+ end
39
+ end
40
+
41
+ # EitherCalculator instance
42
+ c = EitherCalculator.new
43
+
44
+ # If everything went right
45
+ # c.calculate(nil)
46
+ # c.calculate({})
47
+ c.calculate({ok?: true})
48
+ # c.calculate({ok?: true, persist?: true})
49
+
50
+ require "pipetree/flow"
51
+
52
+ # 254.186k (± 1.2%) i/s - 1.289M in 5.071256s # pure dry-monads
53
+ # 268.407k (± 1.6%) i/s - 1.353M in 5.043612s # pipetree-flow
54
+
55
+ pipe = Pipetree::Flow[
56
+ Pipetree::Flow::On.new(Pipetree::Flow::Right, ->(last, value, options) { #puts "|>DESERIALIZATION"
57
+ value.nil? ? [Pipetree::Flow::Left, value] : [Pipetree::Flow::Right, value] } ),
58
+ Pipetree::Flow::On.new(Pipetree::Flow::Right, ->(last, value, options) { #puts "|>VALIDATION"
59
+ value[:ok?] ? [Pipetree::Flow::Right, value] : [Pipetree::Flow::Left, value] } ),
60
+ Pipetree::Flow::On.new(Pipetree::Flow::Right, ->(last, value, options) { #puts "|>PERSISTENCE";
61
+ value[:persist?] ? [Pipetree::Flow::Right, value] : [Pipetree::Flow::Left, value] } ),
62
+ Pipetree::Flow::On.new(Pipetree::Flow::Left, ->(last, value, options) { #puts "|| INVALID=CALLBACK";
63
+ [Pipetree::Flow::Left, value] } ),
64
+ Pipetree::Flow::On.new(Pipetree::Flow::Right, ->(last, value, options) { #puts "|>OK=CALLBACK";
65
+ [Pipetree::Flow::Right, value] } ),
66
+ Pipetree::Flow::On.new(Pipetree::Flow::Left, ->(last, value, options) { #puts "|| SECOND-INVALID=CALLBACK";
67
+ [Pipetree::Flow::Left, value] } ),
68
+
69
+ Pipetree::Flow::On.new(Pipetree::Flow::Left, ->(last, value, options) { #puts "|| FIXING IT=CALLBACK";
70
+ [Pipetree::Flow::Right, value] } ),
71
+ ]
72
+
73
+ puts "Pipetree"
74
+ pipe.({ok?: true}, {})
75
+
76
+ # exit
77
+
78
+
79
+ flow = Pipetree::Flow[]
80
+ flow.& ->(value, options) {
81
+ #puts "|>DESERIALIZATION"
82
+ !value.nil? }
83
+ flow.& ->(value, options) {
84
+ #puts "|>VALIDATION"
85
+ value[:ok?] }
86
+ flow.& ->(value, options) {
87
+ #puts "|>PERSISTENCE";
88
+ value[:persist?] }
89
+ flow.< ->(value, options) {
90
+ #puts "< INVALID=CALLBACK";
91
+ value }
92
+ flow.& ->(value, options) {
93
+ #puts "<>OK=CALLBACK";
94
+ value }
95
+ flow.< ->(value, options) {
96
+ #puts "< SECOND-INVALID=CALLBACK";
97
+ value }
98
+ flow.< ->(value, options) {
99
+ #puts "|| FIXING IT=CALLBACK"
100
+ value }
101
+
102
+ puts "FLOW"
103
+ flow.({ok?: true}, {})
104
+
105
+ require "benchmark/ips"
106
+ Benchmark.ips do |x|
107
+ x.report { c.calculate({ok?: true}) }
108
+ x.report { pipe.({ok?: true}, {}) }
109
+ x.report { flow.({ok?: true}, {}) }
110
+ end
111
+
112
+
113
+
114
+
115
+ # puts result = c.calculate
116
+
117
+
118
+ M = Dry::Monads
119
+
120
+ maybe_user = M.Maybe(true).bind do |u|
121
+ M.Maybe(nil).bind do |a|
122
+ M.Maybe(raise)
123
+ end
124
+ end
125
+
126
+ class A
127
+ def call
128
+ end
129
+ end
130
+
131
+ # Benchmark.ips do |x|
132
+ # x.report { A.new(1) } # 3.059M (± 4.4%) i/s - 15.302M in 5.012694s
133
+ # x.report { [A, 1] } # 6.606M (± 2.9%) i/s - 33.042M in 5.007420s
134
+ # end
135
+
136
+ # proc = ->(*) { }
137
+ # a = A.new
138
+ # Benchmark.ips do |x|
139
+ # x.report { proc.is_a?(Proc) }
140
+ # x.report { a.() }
141
+ # end
142
+
143
+
144
+
145
+
146
+ # 258.340k (± 1.0%) i/s - 1.297M in 5.020288s
147
+ # 266.743k (± 0.9%) i/s - 1.345M in 5.043017s
148
+ # 215.273k (± 1.0%) i/s - 1.089M in 5.060944s
149
+
150
+
151
+ class A
152
+ def initialize
153
+ @b = B.new
154
+ end
155
+
156
+ class B
157
+ def b
158
+ "B"
159
+ end
160
+ end
161
+
162
+ def b
163
+ @b.b
164
+ end
165
+ end
166
+
167
+ class One
168
+ def b
169
+ b_internal
170
+ end
171
+
172
+ def b_internal
173
+ "B"
174
+ end
175
+ end
176
+
177
+ Benchmark.ips do |x|
178
+ x.report { A.new.b }
179
+ x.report { One.new.b }
180
+ end
@@ -0,0 +1,59 @@
1
+ require "test_helper"
2
+
3
+ require "benchmark/ips"
4
+ # 0.020000 0.000000 0.020000 ( 0.023401)
5
+ # 0.050000 0.000000 0.050000 ( 0.057883)
6
+ Return = ->(params, payload, read, write) {
7
+ read["bla"]
8
+ write["bla."] = Object
9
+ params
10
+ }
11
+ # Return = ->(params, payload) {
12
+ # payload[:read]["bla"]
13
+ # payload[:write]["bla."] = Object
14
+ # params
15
+ # }
16
+
17
+ pipe = Pipetree[
18
+ *(99999.times.collect{ Return })
19
+ ]
20
+
21
+ # puts Benchmark.measure { pipe.("bla", {}, {}, {}) }
22
+
23
+
24
+
25
+ #---
26
+ # replacing result AND options.
27
+ Return1 = ->(input, options) {
28
+ result = input + "*"
29
+
30
+ result
31
+ }
32
+
33
+ pipe1 = Pipetree[
34
+ *(99999.times.collect{ Return1 })
35
+ ]
36
+
37
+ class Pipetree2 < Pipetree
38
+ def call(input, options)
39
+ inject(input) do |memo, step|
40
+ res, options = evaluate(step, memo, options)
41
+ return(Stop) if Stop == res
42
+ res
43
+ end
44
+
45
+ end
46
+ end
47
+
48
+ Return2 = ->(input, options) {
49
+ [(result = (input + "*")), options]
50
+ }
51
+
52
+ pipe2 = Pipetree2[
53
+ *(99999.times.collect{ Return2 })
54
+ ]
55
+
56
+ Benchmark.ips do |x|
57
+ x.report { pipe1.("", {}) }
58
+ x.report { pipe2.("", {}) }
59
+ end
data/test/flow_test.rb ADDED
@@ -0,0 +1,125 @@
1
+ require "test_helper"
2
+
3
+ require "pipetree/flow"
4
+
5
+ # pipe = Pipetree[
6
+ # Pipetree::OnRight.new( ->(value, options) { #puts "|>DESERIALIZATION"
7
+ # value.nil? ? [Pipetree::Left, value] : [Pipetree::Right, value] } ),
8
+ # Pipetree::OnRight.new( ->(value, options) { #puts "|>VALIDATION"
9
+ # value[:ok?] ? [Pipetree::Right, value] : [Pipetree::Left, value] } ),
10
+ # Pipetree::OnRight.new( ->(value, options) { #puts "|>PERSISTENCE";
11
+ # value[:persist?] ? [Pipetree::Right, value] : [Pipetree::Left, value] } ),
12
+ # Pipetree::OnLeft.new( ->(value, options) { #puts "|| INVALID=CALLBACK";
13
+ # [Pipetree::Left, value] } ),
14
+ # Pipetree::OnRight.new( ->(value, options) { #puts "|>OK=CALLBACK";
15
+ # [Pipetree::Right, value] } ),
16
+ # Pipetree::OnLeft.new( ->(value, options) { #puts "|| SECOND-INVALID=CALLBACK";
17
+ # [Pipetree::Left, value] } ),
18
+
19
+ # Pipetree::OnLeft.new( ->(value, options) { #puts "|| FIXING IT=CALLBACK";
20
+ # [Pipetree::Right, value] } ),
21
+ # ]
22
+
23
+ # puts "Pipetree"
24
+ # pipe.({ok?: true}, {})
25
+
26
+ require "json"
27
+
28
+ class FlowTest < Minitest::Spec
29
+ # TODO: test each function from & to > is only called once!
30
+ # describe "#call" do
31
+ # let (:pipe) { Pipetree::Flow[] }
32
+ # it do
33
+ # pipe.>> ->(*) { puts "snippet" }
34
+ # pipe.({},{})
35
+ # end
36
+ # end
37
+
38
+ A = ->(*) { }
39
+ B = ->(*) { }
40
+
41
+ let (:pipe) { pipe = Pipetree::Flow[]
42
+ pipe.& ->(value, options) { value && options["deserializer.result"] = JSON.parse(value) }
43
+ pipe.& ->(value, options) { options["deserializer.result"]["key"] == 1 ? true : (options["contract.errors"]=false) }
44
+ pipe.& ->(value, options) { options["deserializer.result"]["key2"] == 2 ? true : (options["contract.errors.2"]="screwd";false) }
45
+ pipe.< ->(value, options) { options["after_deserialize.fail"]=true }
46
+ pipe.% ->(value, options) { options["meantime"] = true }
47
+ pipe.< ->(value, options) { options["after_meantime.left?"]=true; false } # false is ignored.
48
+
49
+ }
50
+
51
+ # success?
52
+ it do
53
+ options = {}
54
+ pipe.(%{{"key": 1,"key2":2}}, options)#.must_equal ""
55
+
56
+ options.must_equal({"deserializer.result"=>{"key"=>1, "key2"=>2}, "meantime"=>true})
57
+ end
58
+
59
+ # invalid?
60
+ it do
61
+ options = {}
62
+ pipe.(%{{"key": 2}}, options)#.must_equal ""
63
+
64
+ options.must_equal({"deserializer.result"=>{"key"=>2}, "contract.errors"=>false, "after_deserialize.fail"=>true, "meantime"=>true, "after_meantime.left?"=>true})
65
+ end
66
+
67
+ it "what" do
68
+ options = {}
69
+ pipe.(%{{"key": 1,"key2":null}}, options)#.must_equal ""
70
+
71
+ options.must_equal({"deserializer.result"=>{"key"=>1, "key2"=>nil}, "contract.errors.2"=>"screwd", "after_deserialize.fail"=>true, "meantime"=>true, "after_meantime.left?"=>true})
72
+ end
73
+
74
+ #---
75
+ # return value is new input.
76
+ it do
77
+ pipe = Pipetree::Flow[
78
+ Pipetree::Flow::On.new(Pipetree::Flow::Right, ->(last, value, options) { [Pipetree::Flow::Right, value.reverse] } )
79
+ ]
80
+ pipe.("Hello", {}).must_equal [Pipetree::Flow::Right, "olleH"]
81
+ end
82
+
83
+ #---
84
+ # #>
85
+ describe "#>" do
86
+ let (:pipe) { Pipetree::Flow[] }
87
+ it {
88
+ pipe.> ->(input, options) { input.reverse }
89
+ # pipe.| B
90
+ # pipe.% A
91
+ pipe.("Hallo", {}).must_equal [Pipetree::Flow::Right, "Hallo"]
92
+ }
93
+ end
94
+
95
+ # #>>
96
+ describe "#>>" do
97
+ let (:pipe) { Pipetree::Flow[] }
98
+ it {
99
+ pipe.>> ->(input, options) { input.reverse }
100
+ pipe.("Hallo", {}).must_equal [Pipetree::Flow::Right, "ollaH"]
101
+ }
102
+ end
103
+
104
+ #---
105
+ # #inspect
106
+ describe "#inspect" do
107
+ let (:pipe) { Pipetree::Flow[].&(A).<(B).%(A) }
108
+
109
+ it { pipe.inspect.must_equal %{[&A,<B,%A]} }
110
+
111
+ it { pipe.inspect(style: :rows).must_equal %{
112
+ 0 &A
113
+ 1 <B
114
+ 2 %A} }
115
+ end
116
+
117
+ describe "#index" do
118
+ let (:pipe) { Pipetree::Flow[].&(A).<(B).%(A) }
119
+
120
+ it { pipe.index(B).must_equal 1 }
121
+ it { pipe.index(A).must_equal 0 }
122
+ end
123
+ end
124
+
125
+ # TODO: instead of testing #index, test all options like :before, etc.
@@ -0,0 +1,33 @@
1
+ require "test_helper"
2
+
3
+ class InspectTest < Minitest::Spec
4
+ module M
5
+ end
6
+
7
+ M::AlphaConstant = ->(*) { }
8
+ M::Beta = ->(*) { }
9
+
10
+ let (:pipe) { ::Pipetree[M::Beta, M::AlphaConstant, M::Beta, M::AlphaConstant, M::Beta, M::AlphaConstant, M::Beta, M::AlphaConstant, M::Beta, M::AlphaConstant, M::Beta] }
11
+
12
+ it do
13
+ puts pipe.inspect
14
+ puts pipe.inspect(style: :rows)
15
+
16
+
17
+ pipe.inspect(style: :rows).must_equal %{
18
+ 0|>M::Beta
19
+ 1|>M::AlphaConstant
20
+ 2|>M::Beta
21
+ 3|>M::AlphaConstant
22
+ 4|>M::Beta
23
+ 5|>M::AlphaConstant
24
+ 6|>M::Beta
25
+ 7|>M::AlphaConstant
26
+ 8|>M::Beta
27
+ 9|>M::AlphaConstant
28
+ 10|>M::Beta}
29
+ end
30
+
31
+ # different separator
32
+ it { ::Pipetree[M::AlphaConstant,M::Beta].inspect.must_equal %{[M::AlphaConstant|>M::Beta]} }
33
+ end
@@ -0,0 +1,200 @@
1
+ require "test_helper"
2
+
3
+ class PipelineTest < MiniTest::Spec
4
+ Song = Struct.new(:title, :artist)
5
+ Artist = Struct.new(:name)
6
+ Album = Struct.new(:ratings, :artists)
7
+
8
+
9
+
10
+ Getter = ->(input, options) { "Yo" }
11
+ StopOnNil = ->(input, options) { input }
12
+ SkipRender = ->(input, *) { input == "Yo" ? input : P::Stop }
13
+
14
+ Prepare = ->(input, options) { "Prepare(#{input})" }
15
+ Deserialize = ->(input, options) { "Deserialize(#{input}, #{options[:fragment]})" }
16
+
17
+ SkipParse = ->(input, options) { input }
18
+ CreateObject = ->(input, options) { OpenStruct.new }
19
+
20
+
21
+ Setter = ->(input, options) { "Setter(#{input})" }
22
+
23
+ AssignFragment = ->(input, options) { options[:fragment] = input }
24
+
25
+ it "linear" do
26
+ Pipetree[SkipParse, Setter].("doc", {fragment: 1}).must_equal "Setter(doc)"
27
+
28
+
29
+ # parse style.
30
+ Pipetree[AssignFragment, SkipParse, CreateObject, Prepare].("Bla", {}).must_equal "Prepare(#<OpenStruct>)"
31
+
32
+
33
+ # render style.
34
+ Pipetree[Getter, StopOnNil, SkipRender, Prepare, Setter].(nil, {}).
35
+ must_equal "Setter(Prepare(Yo))"
36
+
37
+ # pipeline = Representable::Pipeline[SkipParse , SetResult, ModifyResult]
38
+ # pipeline.(fragment: "yo!").must_equal "modified object from yo!"
39
+ end
40
+
41
+ Stopping = ->(input, options) { return Pipetree::Stop if options[:fragment] == "stop!"; input }
42
+
43
+
44
+ it "stopping" do
45
+ pipeline = Pipetree[SkipParse, Stopping, Prepare]
46
+ pipeline.(nil, fragment: "oy!").must_equal "Prepare()"
47
+ pipeline.(nil, fragment: "stop!").must_equal Pipetree::Stop
48
+ end
49
+
50
+ describe "Collect" do
51
+ Reverse = ->(input, options) { input.reverse }
52
+ Add = ->(input, options) { "#{input}+" }
53
+ let(:pipeline) { Pipetree::Collect[Reverse, Add] }
54
+
55
+ it { pipeline.(["yo!", "oy!"], {}).must_equal ["!oy+", "!yo+"] }
56
+
57
+ describe "Pipeline with Collect" do
58
+ let(:pipeline) { Pipetree[Reverse, Pipetree::Collect[Reverse, Add]] }
59
+ it { pipeline.(["yo!", "oy!"], {}).must_equal ["!yo+", "!oy+"] }
60
+ end
61
+ end
62
+
63
+ #---
64
+ # >1 arguments
65
+
66
+ First = ->(input, options) { input }
67
+ Second = ->(input, options, memory) { input }
68
+
69
+ it do
70
+ skip
71
+ Pipetree[First, Second].("", options={}, memory={})
72
+ end
73
+
74
+ # ######### collection :ratings
75
+
76
+ # let (:ratings) {
77
+ # dfn = R::Definition.new(:ratings, collection: true, skip_render: ->(*) { false })
78
+
79
+ # R::Hash::Binding::Collection.new(dfn)
80
+ # }
81
+ # it "render scalar collection" do
82
+ # doc = {}
83
+ # P[
84
+ # R::GetValue,
85
+ # R::StopOnSkipable,
86
+ # R::Collect[
87
+ # R::SkipRender,
88
+ # ],
89
+ # R::AssignName,
90
+ # R::WriteFragment
91
+ # ].extend(P::Debug).(nil, {represented: Album.new([1,2,3]), binding: ratings, doc: doc, options: {}}).must_equal([1,2,3])
92
+
93
+ # doc.must_equal({"ratings"=>[1,2,3]})
94
+ # end
95
+
96
+ # ######### collection :songs, extend: SongRepresenter
97
+ # let (:artists) {
98
+ # dfn = R::Definition.new(:artists, collection: true, extend: ArtistRepresenter, class: Artist)
99
+
100
+ # R::Hash::Binding::Collection.new(dfn)
101
+ # }
102
+ # it "render typed collection" do
103
+ # doc = {}
104
+ # P[
105
+ # R::GetValue,
106
+ # R::StopOnSkipable,
107
+ # R::Collect[
108
+ # R::Decorate,
109
+ # R::Serialize,
110
+ # ],
111
+ # R::AssignName,
112
+ # R::WriteFragment
113
+ # ].extend(P::Debug).(nil, {represented: Album.new(nil, [Artist.new("Diesel Boy"), Artist.new("Van Halen")]), binding: artists, doc: doc, options: {}}).must_equal([{"name"=>"Diesel Boy"}, {"name"=>"Van Halen"}])
114
+
115
+ # doc.must_equal({"artists"=>[{"name"=>"Diesel Boy"}, {"name"=>"Van Halen"}]})
116
+ # end
117
+
118
+ # let (:album_model) { Album.new(nil, [Artist.new("Diesel Boy"), Artist.new("Van Halen")]) }
119
+
120
+ # it "parse typed collection" do
121
+ # doc = {"artists"=>[{"name"=>"Diesel Boy"}, {"name"=>"Van Halen"}]}
122
+ # P[
123
+ # R::AssignName,
124
+ # R::ReadFragment,
125
+ # R::StopOnNotFound,
126
+ # R::OverwriteOnNil,
127
+ # # R::SkipParse,
128
+ # R::Collect[
129
+ # R::AssignFragment,
130
+ # R::CreateObject::Class,
131
+ # R::Decorate,
132
+ # R::Deserialize,
133
+ # ],
134
+ # R::SetValue,
135
+ # ].extend(P::Debug).(doc, {represented: album_model, binding: artists, doc: doc, options: {}}).must_equal([Artist.new("Diesel Boy"), Artist.new("Van Halen")])
136
+
137
+ # album_model.artists.must_equal([Artist.new("Diesel Boy"), Artist.new("Van Halen")])
138
+ # end
139
+
140
+ # # TODO: test with arrays, too, not "only" Pipeline instances.
141
+ # describe "#Insert Pipeline[], Function, replace: OldFunction" do
142
+ # let (:pipeline) { P[R::GetValue, R::StopOnSkipable, R::StopOnNil] }
143
+
144
+ # it "returns Pipeline instance when passing in Pipeline instance" do
145
+ # P::Insert.(pipeline, R::Default, replace: R::StopOnSkipable).must_be_instance_of(R::Pipeline)
146
+ # end
147
+
148
+ # it "replaces if exists" do
149
+ # # pipeline.insert!(R::Default, replace: R::StopOnSkipable)
150
+ # P::Insert.(pipeline, R::Default, replace: R::StopOnSkipable).must_equal P[R::GetValue, R::Default, R::StopOnNil]
151
+ # pipeline.must_equal P[R::GetValue, R::StopOnSkipable, R::StopOnNil]
152
+ # end
153
+
154
+ # it "replaces Function instance" do
155
+ # pipeline = P[R::Prepare, R::StopOnSkipable, R::StopOnNil]
156
+ # P::Insert.(pipeline, R::Default, replace: R::Prepare).must_equal P[R::Default, R::StopOnSkipable, R::StopOnNil]
157
+ # pipeline.must_equal P[R::Prepare, R::StopOnSkipable, R::StopOnNil]
158
+ # end
159
+
160
+ # it "does not replace when not existing" do
161
+ # P::Insert.(pipeline, R::Default, replace: R::Prepare)
162
+ # pipeline.must_equal P[R::GetValue, R::StopOnSkipable, R::StopOnNil]
163
+ # end
164
+
165
+ # it "applies on nested Collect" do
166
+ # pipeline = P[R::GetValue, R::Collect[R::GetValue, R::StopOnSkipable], R::StopOnNil]
167
+
168
+ # P::Insert.(pipeline, R::Default, replace: R::StopOnSkipable).extend(P::Debug).inspect.must_equal "Pipeline[GetValue, Collect[GetValue, Default], StopOnNil]"
169
+ # pipeline.must_equal P[R::GetValue, R::Collect[R::GetValue, R::StopOnSkipable], R::StopOnNil]
170
+
171
+
172
+ # P::Insert.(pipeline, R::Default, replace: R::StopOnNil).extend(P::Debug).inspect.must_equal "Pipeline[GetValue, Collect[GetValue, StopOnSkipable], Default]"
173
+ # end
174
+
175
+ # it "applies on nested Collect with Function::CreateObject" do
176
+ # pipeline = P[R::GetValue, R::Collect[R::GetValue, R::CreateObject], R::StopOnNil]
177
+
178
+ # P::Insert.(pipeline, R::Default, replace: R::CreateObject).extend(P::Debug).inspect.must_equal "Pipeline[GetValue, Collect[GetValue, Default], StopOnNil]"
179
+ # pipeline.must_equal P[R::GetValue, R::Collect[R::GetValue, R::CreateObject], R::StopOnNil]
180
+ # end
181
+ # end
182
+
183
+ # describe "Insert.(delete: true)" do
184
+ # let(:pipeline) { P[R::GetValue, R::StopOnNil] }
185
+
186
+ # it do
187
+ # P::Insert.(pipeline, R::GetValue, delete: true).extend(P::Debug).inspect.must_equal "Pipeline[StopOnNil]"
188
+ # pipeline.extend(P::Debug).inspect.must_equal "Pipeline[GetValue, StopOnNil]"
189
+ # end
190
+ # end
191
+
192
+ # describe "Insert.(delete: true) with Collect" do
193
+ # let(:pipeline) { P[R::GetValue, R::Collect[R::GetValue, R::StopOnSkipable], R::StopOnNil] }
194
+
195
+ # it do
196
+ # P::Insert.(pipeline, R::GetValue, delete: true).extend(P::Debug).inspect.must_equal "Pipeline[Collect[StopOnSkipable], StopOnNil]"
197
+ # pipeline.extend(P::Debug).inspect.must_equal "Pipeline[GetValue, Collect[GetValue, StopOnSkipable], StopOnNil]"
198
+ # end
199
+ # end
200
+ end
@@ -0,0 +1,2 @@
1
+ require "minitest/autorun"
2
+ require "pipetree"
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: pipetree
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.0.1
4
+ version: 0.0.2
5
5
  platform: ruby
6
6
  authors:
7
7
  - Nick Sutterer
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2016-07-05 00:00:00.000000000 Z
11
+ date: 2016-11-01 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: bundler
@@ -59,9 +59,24 @@ executables: []
59
59
  extensions: []
60
60
  extra_rdoc_files: []
61
61
  files:
62
+ - CHANGES.md
63
+ - Gemfile
64
+ - README.md
65
+ - Rakefile
62
66
  - lib/pipetree.rb
67
+ - lib/pipetree/flow.rb
68
+ - lib/pipetree/flow/inspect.rb
69
+ - lib/pipetree/insert.rb
70
+ - lib/pipetree/inspect.rb
63
71
  - lib/pipetree/version.rb
64
72
  - pipetree.gemspec
73
+ - test/altering_test.rb
74
+ - test/benchmark_monads.rb
75
+ - test/benchmarking.rb
76
+ - test/flow_test.rb
77
+ - test/inspect_test.rb
78
+ - test/pipetree_test.rb
79
+ - test/test_helper.rb
65
80
  homepage: https://github.com/apotonick/pipetree
66
81
  licenses:
67
82
  - MIT
@@ -82,9 +97,16 @@ required_rubygems_version: !ruby/object:Gem::Requirement
82
97
  version: '0'
83
98
  requirements: []
84
99
  rubyforge_project:
85
- rubygems_version: 2.4.8
100
+ rubygems_version: 2.6.3
86
101
  signing_key:
87
102
  specification_version: 4
88
103
  summary: Functional nested pipeline dialect that reduces runtime logic, overriding
89
104
  and awkward module inclusion.
90
- test_files: []
105
+ test_files:
106
+ - test/altering_test.rb
107
+ - test/benchmark_monads.rb
108
+ - test/benchmarking.rb
109
+ - test/flow_test.rb
110
+ - test/inspect_test.rb
111
+ - test/pipetree_test.rb
112
+ - test/test_helper.rb