pipetree 0.0.1 → 0.0.2

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 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