musa-dsl 0.14.16
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +7 -0
- data/.gitignore +10 -0
- data/Gemfile +20 -0
- data/LICENSE.md +157 -0
- data/README.md +8 -0
- data/lib/musa-dsl/core-ext/array-apply-get.rb +18 -0
- data/lib/musa-dsl/core-ext/array-explode-ranges.rb +29 -0
- data/lib/musa-dsl/core-ext/array-to-neumas.rb +28 -0
- data/lib/musa-dsl/core-ext/array-to-serie.rb +20 -0
- data/lib/musa-dsl/core-ext/arrayfy.rb +15 -0
- data/lib/musa-dsl/core-ext/as-context-run.rb +44 -0
- data/lib/musa-dsl/core-ext/duplicate.rb +134 -0
- data/lib/musa-dsl/core-ext/dynamic-proxy.rb +55 -0
- data/lib/musa-dsl/core-ext/inspect-nice.rb +28 -0
- data/lib/musa-dsl/core-ext/key-parameters-procedure-binder.rb +85 -0
- data/lib/musa-dsl/core-ext/proc-nice.rb +13 -0
- data/lib/musa-dsl/core-ext/send-nice.rb +21 -0
- data/lib/musa-dsl/core-ext/string-to-neumas.rb +27 -0
- data/lib/musa-dsl/core-ext.rb +13 -0
- data/lib/musa-dsl/datasets/gdv-decorators.rb +221 -0
- data/lib/musa-dsl/datasets/gdv.rb +499 -0
- data/lib/musa-dsl/datasets/pdv.rb +44 -0
- data/lib/musa-dsl/datasets.rb +5 -0
- data/lib/musa-dsl/generative/darwin.rb +145 -0
- data/lib/musa-dsl/generative/generative-grammar.rb +294 -0
- data/lib/musa-dsl/generative/markov.rb +78 -0
- data/lib/musa-dsl/generative/rules.rb +282 -0
- data/lib/musa-dsl/generative/variatio.rb +331 -0
- data/lib/musa-dsl/generative.rb +5 -0
- data/lib/musa-dsl/midi/midi-recorder.rb +83 -0
- data/lib/musa-dsl/midi/midi-voices.rb +274 -0
- data/lib/musa-dsl/midi.rb +2 -0
- data/lib/musa-dsl/music/chord-definition.rb +99 -0
- data/lib/musa-dsl/music/chord-definitions.rb +13 -0
- data/lib/musa-dsl/music/chords.rb +326 -0
- data/lib/musa-dsl/music/equally-tempered-12-tone-scale-system.rb +204 -0
- data/lib/musa-dsl/music/scales.rb +584 -0
- data/lib/musa-dsl/music.rb +6 -0
- data/lib/musa-dsl/neuma/neuma.rb +181 -0
- data/lib/musa-dsl/neuma.rb +1 -0
- data/lib/musa-dsl/neumalang/neumalang.citrus +294 -0
- data/lib/musa-dsl/neumalang/neumalang.rb +179 -0
- data/lib/musa-dsl/neumalang.rb +3 -0
- data/lib/musa-dsl/repl/repl.rb +143 -0
- data/lib/musa-dsl/repl.rb +1 -0
- data/lib/musa-dsl/sequencer/base-sequencer-implementation-control.rb +189 -0
- data/lib/musa-dsl/sequencer/base-sequencer-implementation-play-helper.rb +354 -0
- data/lib/musa-dsl/sequencer/base-sequencer-implementation.rb +382 -0
- data/lib/musa-dsl/sequencer/base-sequencer-public.rb +261 -0
- data/lib/musa-dsl/sequencer/sequencer-dsl.rb +94 -0
- data/lib/musa-dsl/sequencer/sequencer.rb +3 -0
- data/lib/musa-dsl/sequencer.rb +1 -0
- data/lib/musa-dsl/series/base-series.rb +245 -0
- data/lib/musa-dsl/series/hash-serie-splitter.rb +194 -0
- data/lib/musa-dsl/series/holder-serie.rb +87 -0
- data/lib/musa-dsl/series/main-serie-constructors.rb +726 -0
- data/lib/musa-dsl/series/main-serie-operations.rb +1151 -0
- data/lib/musa-dsl/series/proxy-serie.rb +69 -0
- data/lib/musa-dsl/series/queue-serie.rb +94 -0
- data/lib/musa-dsl/series/series.rb +8 -0
- data/lib/musa-dsl/series.rb +1 -0
- data/lib/musa-dsl/transport/clock.rb +36 -0
- data/lib/musa-dsl/transport/dummy-clock.rb +47 -0
- data/lib/musa-dsl/transport/external-tick-clock.rb +31 -0
- data/lib/musa-dsl/transport/input-midi-clock.rb +124 -0
- data/lib/musa-dsl/transport/timer-clock.rb +102 -0
- data/lib/musa-dsl/transport/timer.rb +40 -0
- data/lib/musa-dsl/transport/transport.rb +137 -0
- data/lib/musa-dsl/transport.rb +9 -0
- data/lib/musa-dsl.rb +17 -0
- data/musa-dsl.gemspec +17 -0
- metadata +174 -0
@@ -0,0 +1,294 @@
|
|
1
|
+
module Musa
|
2
|
+
module GenerativeGrammar
|
3
|
+
|
4
|
+
def N(content = nil, **attributes, &block)
|
5
|
+
if block_given? && content.nil?
|
6
|
+
BlockNode.new(attributes, &block)
|
7
|
+
else
|
8
|
+
FinalNode.new(content, attributes)
|
9
|
+
end
|
10
|
+
end
|
11
|
+
|
12
|
+
class OptionElement
|
13
|
+
attr_reader :content, :attributes
|
14
|
+
|
15
|
+
def initialize(content, attributes = nil)
|
16
|
+
@content = content
|
17
|
+
@attributes = attributes || {}
|
18
|
+
end
|
19
|
+
end
|
20
|
+
|
21
|
+
class Node
|
22
|
+
def or(other)
|
23
|
+
OrNode.new(self, other)
|
24
|
+
end
|
25
|
+
|
26
|
+
alias_method :|, :or
|
27
|
+
|
28
|
+
def repeat(exactly = nil, min: nil, max: nil)
|
29
|
+
raise ArgumentError, 'Only exactly value or min/max values are allowed' if exactly && (min || max)
|
30
|
+
|
31
|
+
min = max = exactly if exactly
|
32
|
+
|
33
|
+
if min && min > 0
|
34
|
+
pre = self
|
35
|
+
|
36
|
+
(min - 1).times do
|
37
|
+
pre += self
|
38
|
+
end
|
39
|
+
end
|
40
|
+
|
41
|
+
if pre && max == min
|
42
|
+
pre
|
43
|
+
elsif pre && max > min
|
44
|
+
pre + RepeatNode.new(self, max - min)
|
45
|
+
else
|
46
|
+
RepeatNode.new(self, max)
|
47
|
+
end
|
48
|
+
end
|
49
|
+
|
50
|
+
def limit(attribute = nil, after_collect_operation = nil, comparison_method = nil, comparison_value = nil, &block)
|
51
|
+
raise ArgumentError, 'Cannot use simplified arguments and yield block at the same time' if (attribute || after_collect_operation || comparison_method || comparison_value) && @block
|
52
|
+
|
53
|
+
block ||= generate_simple_condition_block(attribute, after_collect_operation, comparison_method, comparison_value)
|
54
|
+
|
55
|
+
ConditionNode.new(self, &block)
|
56
|
+
end
|
57
|
+
|
58
|
+
def next(other)
|
59
|
+
NextNode.new(self, other)
|
60
|
+
end
|
61
|
+
|
62
|
+
alias_method :+, :next
|
63
|
+
|
64
|
+
def options(attribute = nil,
|
65
|
+
after_collect_operation = nil,
|
66
|
+
comparison_method = nil,
|
67
|
+
comparison_value = nil,
|
68
|
+
raw: nil,
|
69
|
+
content: nil,
|
70
|
+
&condition)
|
71
|
+
|
72
|
+
raise ArgumentError, 'Cannot use simplified arguments and yield block at the same time' if (attribute || after_collect_operation || comparison_method || comparison_value) && @condition
|
73
|
+
raise ArgumentError, 'Cannot use raw: true and content: option at the same time' if raw && content
|
74
|
+
|
75
|
+
raw ||= false
|
76
|
+
content ||= :itself
|
77
|
+
|
78
|
+
condition ||= generate_simple_condition_block(attribute, after_collect_operation, comparison_method, comparison_value)
|
79
|
+
|
80
|
+
if raw
|
81
|
+
_options(&condition)
|
82
|
+
else
|
83
|
+
_options(&condition).collect { |o| o.collect(&:content).send(content) }
|
84
|
+
end
|
85
|
+
end
|
86
|
+
|
87
|
+
def size
|
88
|
+
options.size
|
89
|
+
end
|
90
|
+
|
91
|
+
alias_method :length, :size
|
92
|
+
|
93
|
+
def [](index)
|
94
|
+
options[index].to_serie.to_node
|
95
|
+
end
|
96
|
+
|
97
|
+
def to_serie(flatten: nil, &condition)
|
98
|
+
flatten ||= true
|
99
|
+
|
100
|
+
serie = _options(&condition).collect { |o| o.collect(&:content) }.to_serie(of_series: true).merge
|
101
|
+
serie = serie.flatten if flatten
|
102
|
+
|
103
|
+
serie.prototype
|
104
|
+
end
|
105
|
+
|
106
|
+
alias_method :s, :to_serie
|
107
|
+
|
108
|
+
def _options(parent: nil, &condition)
|
109
|
+
raise NotImplementedError
|
110
|
+
end
|
111
|
+
|
112
|
+
protected
|
113
|
+
|
114
|
+
def generate_simple_condition_block(attribute = nil,
|
115
|
+
after_collect_operation = nil,
|
116
|
+
comparison_method = nil,
|
117
|
+
comparison_value = nil)
|
118
|
+
|
119
|
+
if attribute && after_collect_operation && comparison_method && comparison_value
|
120
|
+
proc do |o|
|
121
|
+
o.collect { |_| _.attributes[attribute] }
|
122
|
+
.send(after_collect_operation)
|
123
|
+
.send(comparison_method, comparison_value)
|
124
|
+
end
|
125
|
+
end
|
126
|
+
end
|
127
|
+
end
|
128
|
+
|
129
|
+
private_constant :Node
|
130
|
+
|
131
|
+
class ProxyNode < Node
|
132
|
+
attr_accessor :node
|
133
|
+
|
134
|
+
def _options(parent: nil, &condition)
|
135
|
+
@node._options parent: parent, &condition
|
136
|
+
end
|
137
|
+
end
|
138
|
+
|
139
|
+
class FinalNode < Node
|
140
|
+
attr_reader :content
|
141
|
+
attr_reader :attributes
|
142
|
+
|
143
|
+
def initialize(content, attributes)
|
144
|
+
super()
|
145
|
+
@element = OptionElement.new(content, attributes)
|
146
|
+
end
|
147
|
+
|
148
|
+
def _options(parent: nil, &condition)
|
149
|
+
parent ||= []
|
150
|
+
|
151
|
+
if block_given?
|
152
|
+
if yield(parent + [@element])
|
153
|
+
[[@element]]
|
154
|
+
else
|
155
|
+
[]
|
156
|
+
end
|
157
|
+
else
|
158
|
+
[[@element]]
|
159
|
+
end
|
160
|
+
end
|
161
|
+
end
|
162
|
+
|
163
|
+
private_constant :FinalNode
|
164
|
+
|
165
|
+
class BlockNode < Node
|
166
|
+
def initialize(attributes, &block)
|
167
|
+
@attributes = attributes
|
168
|
+
@block = block
|
169
|
+
end
|
170
|
+
|
171
|
+
def _options(parent: nil, &condition)
|
172
|
+
parent ||= []
|
173
|
+
|
174
|
+
element = @block.call(parent, @attributes)
|
175
|
+
element = OptionElement.new(element, @attributes) unless element.is_a?(OptionElement)
|
176
|
+
|
177
|
+
if block_given?
|
178
|
+
if yield(parent + [element], @attributes)
|
179
|
+
[[element]]
|
180
|
+
else
|
181
|
+
[]
|
182
|
+
end
|
183
|
+
else
|
184
|
+
[[element]]
|
185
|
+
end
|
186
|
+
end
|
187
|
+
end
|
188
|
+
|
189
|
+
private_constant :BlockNode
|
190
|
+
|
191
|
+
class ConditionNode < Node
|
192
|
+
def initialize(node, &block)
|
193
|
+
@node = node
|
194
|
+
@block = block
|
195
|
+
end
|
196
|
+
|
197
|
+
def _options(parent: nil, &condition)
|
198
|
+
parent ||= []
|
199
|
+
|
200
|
+
r = []
|
201
|
+
|
202
|
+
@node._options(parent: parent, &condition).each do |node_option|
|
203
|
+
r << node_option if (!block_given? || yield(parent + node_option)) && @block.call(parent + node_option)
|
204
|
+
end
|
205
|
+
|
206
|
+
r
|
207
|
+
end
|
208
|
+
end
|
209
|
+
|
210
|
+
private_constant :ConditionNode
|
211
|
+
|
212
|
+
class OrNode < Node
|
213
|
+
def initialize(node1, node2)
|
214
|
+
@node1 = node1
|
215
|
+
@node2 = node2
|
216
|
+
super()
|
217
|
+
end
|
218
|
+
|
219
|
+
def _options(parent: nil, &condition)
|
220
|
+
parent ||= []
|
221
|
+
|
222
|
+
r = []
|
223
|
+
|
224
|
+
@node1._options(parent: parent, &condition).each do |node_option|
|
225
|
+
r << node_option if !block_given? || yield(parent + node_option)
|
226
|
+
end
|
227
|
+
|
228
|
+
@node2._options(parent: parent, &condition).each do |node_option|
|
229
|
+
r << node_option if !block_given? || yield(parent + node_option)
|
230
|
+
end
|
231
|
+
|
232
|
+
r
|
233
|
+
end
|
234
|
+
end
|
235
|
+
|
236
|
+
private_constant :OrNode
|
237
|
+
|
238
|
+
class NextNode < Node
|
239
|
+
def initialize(node, after)
|
240
|
+
@node = node
|
241
|
+
@after = after
|
242
|
+
super()
|
243
|
+
end
|
244
|
+
|
245
|
+
def _options(parent: nil, &condition)
|
246
|
+
parent ||= []
|
247
|
+
|
248
|
+
r = []
|
249
|
+
@node._options(parent: parent, &condition).each do |node_option|
|
250
|
+
@after._options(parent: parent + node_option, &condition).each do |after_option|
|
251
|
+
r << node_option + after_option unless after_option.empty?
|
252
|
+
end
|
253
|
+
end
|
254
|
+
r
|
255
|
+
end
|
256
|
+
end
|
257
|
+
|
258
|
+
private_constant :NextNode
|
259
|
+
|
260
|
+
class RepeatNode < Node
|
261
|
+
def initialize(node, max = nil)
|
262
|
+
@node = node
|
263
|
+
@max = max
|
264
|
+
|
265
|
+
super()
|
266
|
+
end
|
267
|
+
|
268
|
+
def _options(parent: nil, depth: nil, &condition)
|
269
|
+
parent ||= []
|
270
|
+
depth ||= 0
|
271
|
+
|
272
|
+
r = []
|
273
|
+
|
274
|
+
if @max.nil? || depth < @max
|
275
|
+
node_options = @node._options(parent: parent, &condition)
|
276
|
+
|
277
|
+
node_options.each do |node_option|
|
278
|
+
r << node_option
|
279
|
+
|
280
|
+
node_suboptions = _options(parent: parent + node_option, depth: depth + 1, &condition)
|
281
|
+
|
282
|
+
node_suboptions.each do |node_suboption|
|
283
|
+
r << node_option + node_suboption
|
284
|
+
end
|
285
|
+
end
|
286
|
+
end
|
287
|
+
|
288
|
+
r
|
289
|
+
end
|
290
|
+
end
|
291
|
+
|
292
|
+
private_constant :RepeatNode
|
293
|
+
end
|
294
|
+
end
|
@@ -0,0 +1,78 @@
|
|
1
|
+
require 'musa-dsl/series'
|
2
|
+
|
3
|
+
module Musa
|
4
|
+
|
5
|
+
# TODO: adapt to series prototyping
|
6
|
+
|
7
|
+
class Markov
|
8
|
+
include Serie
|
9
|
+
|
10
|
+
attr_accessor :start, :finish, :random, :transitions
|
11
|
+
|
12
|
+
def initialize(transitions:, start:, finish: nil, random: nil)
|
13
|
+
@transitions = transitions.clone.freeze
|
14
|
+
|
15
|
+
@start = start
|
16
|
+
@finish = finish
|
17
|
+
|
18
|
+
@random = Random.new random if random.is_a?(Integer)
|
19
|
+
@random ||= Random.new
|
20
|
+
|
21
|
+
@procedure_binders = {}
|
22
|
+
|
23
|
+
_restart
|
24
|
+
end
|
25
|
+
|
26
|
+
def _restart
|
27
|
+
@current = nil
|
28
|
+
@finished = false
|
29
|
+
@history = []
|
30
|
+
end
|
31
|
+
|
32
|
+
def _next_value
|
33
|
+
if @finished
|
34
|
+
@current = nil
|
35
|
+
else
|
36
|
+
if @current.nil?
|
37
|
+
@current = @start
|
38
|
+
else
|
39
|
+
if @transitions.has_key? @current
|
40
|
+
options = @transitions[@current]
|
41
|
+
|
42
|
+
case options
|
43
|
+
when Array
|
44
|
+
@current = options[@random.rand(0...options.size)]
|
45
|
+
|
46
|
+
when Hash
|
47
|
+
total = accumulated = 0.0
|
48
|
+
options.each_value { |probability| total += probability.abs }
|
49
|
+
r = @random.rand total
|
50
|
+
|
51
|
+
@current = options.find { |key, probability|
|
52
|
+
accumulated += probability;
|
53
|
+
r >= accumulated - probability && r < accumulated }[0]
|
54
|
+
|
55
|
+
when Proc
|
56
|
+
procedure_binder = @procedure_binders[options] ||= KeyParametersProcedureBinder.new options
|
57
|
+
@current = procedure_binder.call @history
|
58
|
+
else
|
59
|
+
raise ArgumentError, "Option #{option} is not allowed. Only Array, Hash or Proc are allowed."
|
60
|
+
end
|
61
|
+
else
|
62
|
+
raise RuntimeError, "No transition defined for #{@current}"
|
63
|
+
end
|
64
|
+
end
|
65
|
+
|
66
|
+
@history << @current
|
67
|
+
@finished = true if !@finish.nil? && (@current == @finish)
|
68
|
+
end
|
69
|
+
|
70
|
+
@current
|
71
|
+
end
|
72
|
+
|
73
|
+
def infinite?
|
74
|
+
@finish.nil?
|
75
|
+
end
|
76
|
+
end
|
77
|
+
end
|
78
|
+
|
@@ -0,0 +1,282 @@
|
|
1
|
+
require 'musa-dsl/core-ext/as-context-run'
|
2
|
+
require 'musa-dsl/core-ext/key-parameters-procedure-binder'
|
3
|
+
|
4
|
+
module Musa
|
5
|
+
class Rules
|
6
|
+
def initialize(&block)
|
7
|
+
@context = RulesEvalContext.new.tap { |_| _._as_context_run block }
|
8
|
+
end
|
9
|
+
|
10
|
+
def generate_possibilities(object, confirmed_node = nil, node = nil, rules = nil)
|
11
|
+
node ||= Node.new
|
12
|
+
rules ||= @context._rules
|
13
|
+
|
14
|
+
history = confirmed_node.history if confirmed_node
|
15
|
+
history ||= []
|
16
|
+
|
17
|
+
rules = rules.clone
|
18
|
+
rule = rules.shift
|
19
|
+
|
20
|
+
if rule
|
21
|
+
rule.generate_possibilities(object, history).each do |new_object|
|
22
|
+
new_node = Node.new new_object, node
|
23
|
+
new_node.mark_as_ended! if @context._ended? new_object
|
24
|
+
|
25
|
+
rejection = @context._rejections.find { |rejection| rejection.rejects?(new_object, history) }
|
26
|
+
# TODO: include rejection secondary reasons in rejection message
|
27
|
+
|
28
|
+
new_node.reject! rejection if rejection
|
29
|
+
|
30
|
+
node.children << new_node
|
31
|
+
end
|
32
|
+
end
|
33
|
+
|
34
|
+
unless rules.empty?
|
35
|
+
node.children.each do |node|
|
36
|
+
generate_possibilities node.object, confirmed_node, node, rules unless node.rejected || node.ended?
|
37
|
+
end
|
38
|
+
end
|
39
|
+
|
40
|
+
node
|
41
|
+
end
|
42
|
+
|
43
|
+
def apply(object_or_list, node = nil)
|
44
|
+
list = object_or_list.arrayfy.clone
|
45
|
+
|
46
|
+
node ||= Node.new
|
47
|
+
|
48
|
+
seed = list.shift
|
49
|
+
|
50
|
+
if seed
|
51
|
+
result = generate_possibilities seed, node
|
52
|
+
|
53
|
+
fished = result.fish
|
54
|
+
|
55
|
+
node.reject! 'All children are rejected' if fished.empty?
|
56
|
+
|
57
|
+
fished.each do |object|
|
58
|
+
subnode = node.add(object).mark_as_ended!
|
59
|
+
apply list, subnode
|
60
|
+
end
|
61
|
+
end
|
62
|
+
|
63
|
+
node
|
64
|
+
end
|
65
|
+
|
66
|
+
class RulesEvalContext
|
67
|
+
attr_reader :_rules, :_ended_when, :_rejections
|
68
|
+
|
69
|
+
def rule(name, &block)
|
70
|
+
@_rules ||= []
|
71
|
+
@_rules << Rule.new(name, self, block)
|
72
|
+
self
|
73
|
+
end
|
74
|
+
|
75
|
+
def ended_when(&block)
|
76
|
+
@_ended_when = block
|
77
|
+
self
|
78
|
+
end
|
79
|
+
|
80
|
+
def rejection(reason, &block)
|
81
|
+
@_rejections ||= []
|
82
|
+
@_rejections << Rejection.new(reason, self, block)
|
83
|
+
self
|
84
|
+
end
|
85
|
+
|
86
|
+
def _ended?(object)
|
87
|
+
as_context_run @_ended_when, object
|
88
|
+
end
|
89
|
+
|
90
|
+
class Rule
|
91
|
+
attr_reader :name
|
92
|
+
|
93
|
+
def initialize(name, context, block)
|
94
|
+
@name = name
|
95
|
+
@context = context
|
96
|
+
@block = block
|
97
|
+
end
|
98
|
+
|
99
|
+
def generate_possibilities(object, history)
|
100
|
+
# TODO: optimize context using only one instance for all genereate_possibilities calls
|
101
|
+
context = RuleEvalContext.new @context
|
102
|
+
context.as_context_run @block, object, history
|
103
|
+
|
104
|
+
context._possibilities
|
105
|
+
end
|
106
|
+
|
107
|
+
class RuleEvalContext
|
108
|
+
attr_reader :_possibilities
|
109
|
+
|
110
|
+
def initialize(parent_context)
|
111
|
+
@_parent_context = parent_context
|
112
|
+
@_possibilities = []
|
113
|
+
end
|
114
|
+
|
115
|
+
def possibility(object)
|
116
|
+
@_possibilities << object
|
117
|
+
self
|
118
|
+
end
|
119
|
+
|
120
|
+
private
|
121
|
+
|
122
|
+
def method_missing(method_name, *args, **key_args, &block)
|
123
|
+
if @_parent_context.respond_to? method_name
|
124
|
+
@_parent_context.send_nice method_name, *args, **key_args, &block
|
125
|
+
else
|
126
|
+
super
|
127
|
+
end
|
128
|
+
end
|
129
|
+
|
130
|
+
def respond_to_missing?(method_name, include_private)
|
131
|
+
@_parent_context.respond_to?(method_name, include_private) || super
|
132
|
+
end
|
133
|
+
end
|
134
|
+
|
135
|
+
private_constant :RuleEvalContext
|
136
|
+
end
|
137
|
+
|
138
|
+
private_constant :Rule
|
139
|
+
|
140
|
+
class Rejection
|
141
|
+
attr_reader :reason
|
142
|
+
|
143
|
+
def initialize(reason, context = nil, block = nil)
|
144
|
+
@reason = reason
|
145
|
+
@context = context
|
146
|
+
@block = block
|
147
|
+
end
|
148
|
+
|
149
|
+
def rejects?(object, history)
|
150
|
+
# TODO: optimize context using only one instance for all rejects? checks
|
151
|
+
context = RejectionEvalContext.new @context
|
152
|
+
context.as_context_run @block, object, history
|
153
|
+
|
154
|
+
reasons = context._secondary_reasons.collect { |_| ("#{@reason} (#{_})" if _) || @reason }
|
155
|
+
|
156
|
+
reasons.empty? ? nil : reasons
|
157
|
+
end
|
158
|
+
|
159
|
+
class RejectionEvalContext
|
160
|
+
attr_reader :_secondary_reasons
|
161
|
+
|
162
|
+
def initialize(parent_context)
|
163
|
+
@_parent_context = parent_context
|
164
|
+
@_secondary_reasons = []
|
165
|
+
end
|
166
|
+
|
167
|
+
def reject(secondary_reason = nil)
|
168
|
+
@_secondary_reasons << secondary_reason
|
169
|
+
self
|
170
|
+
end
|
171
|
+
|
172
|
+
private
|
173
|
+
|
174
|
+
def method_missing(method_name, *args, **key_args, &block)
|
175
|
+
if @_parent_context.respond_to? method_name
|
176
|
+
@_parent_context.send_nice method_name, *args, **key_args, &block
|
177
|
+
else
|
178
|
+
super
|
179
|
+
end
|
180
|
+
end
|
181
|
+
|
182
|
+
def respond_to_missing?(method_name, include_private)
|
183
|
+
@_parent_context.respond_to?(method_name, include_private) || super
|
184
|
+
end
|
185
|
+
end
|
186
|
+
|
187
|
+
private_constant :RejectionEvalContext
|
188
|
+
end
|
189
|
+
|
190
|
+
private_constant :Rejection
|
191
|
+
end
|
192
|
+
|
193
|
+
private_constant :RulesEvalContext
|
194
|
+
|
195
|
+
class Node
|
196
|
+
attr_reader :parent, :children, :object, :rejected
|
197
|
+
|
198
|
+
def initialize(object = nil, parent = nil)
|
199
|
+
@parent = parent
|
200
|
+
@children = []
|
201
|
+
@object = object
|
202
|
+
|
203
|
+
@ended = false
|
204
|
+
@rejected = nil
|
205
|
+
end
|
206
|
+
|
207
|
+
def add(object)
|
208
|
+
Node.new(object, self).tap { |n| @children << n }
|
209
|
+
end
|
210
|
+
|
211
|
+
def reject!(rejection)
|
212
|
+
@rejected = rejection
|
213
|
+
self
|
214
|
+
end
|
215
|
+
|
216
|
+
def mark_as_ended!
|
217
|
+
@children.each(&:update_rejection_by_children!)
|
218
|
+
|
219
|
+
if !@children.empty? && !@children.find { |n| !n.rejected }
|
220
|
+
reject! Rejection::AllChildrenRejected
|
221
|
+
end
|
222
|
+
|
223
|
+
@ended = true
|
224
|
+
|
225
|
+
self
|
226
|
+
end
|
227
|
+
|
228
|
+
def ended?
|
229
|
+
@ended
|
230
|
+
end
|
231
|
+
|
232
|
+
def history
|
233
|
+
objects = []
|
234
|
+
n = self
|
235
|
+
while n && n.object
|
236
|
+
objects << n.object
|
237
|
+
n = n.parent
|
238
|
+
end
|
239
|
+
|
240
|
+
objects.reverse
|
241
|
+
end
|
242
|
+
|
243
|
+
def fish
|
244
|
+
fished = []
|
245
|
+
|
246
|
+
@children.each do |node|
|
247
|
+
unless node.rejected
|
248
|
+
if node.ended?
|
249
|
+
fished << node.object
|
250
|
+
else
|
251
|
+
fished += node.fish
|
252
|
+
end
|
253
|
+
end
|
254
|
+
end
|
255
|
+
|
256
|
+
fished
|
257
|
+
end
|
258
|
+
|
259
|
+
def combinations(parent_combination = nil)
|
260
|
+
parent_combination ||= []
|
261
|
+
|
262
|
+
combinations = []
|
263
|
+
|
264
|
+
unless rejected
|
265
|
+
if @children.empty?
|
266
|
+
combinations << parent_combination
|
267
|
+
else
|
268
|
+
@children.each do |node|
|
269
|
+
node.combinations(parent_combination + [node.object]).each do |object|
|
270
|
+
combinations << object
|
271
|
+
end
|
272
|
+
end
|
273
|
+
end
|
274
|
+
end
|
275
|
+
|
276
|
+
combinations
|
277
|
+
end
|
278
|
+
end
|
279
|
+
|
280
|
+
private_constant :Node
|
281
|
+
end
|
282
|
+
end
|