musa-dsl 0.30.2 → 0.40.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.
- checksums.yaml +4 -4
- data/.gitignore +3 -1
- data/.version +6 -0
- data/.yardopts +7 -0
- data/README.md +227 -6
- data/docs/README.md +83 -0
- data/docs/api-reference.md +86 -0
- data/docs/getting-started/quick-start.md +93 -0
- data/docs/getting-started/tutorial.md +58 -0
- data/docs/subsystems/core-extensions.md +316 -0
- data/docs/subsystems/datasets.md +465 -0
- data/docs/subsystems/generative.md +290 -0
- data/docs/subsystems/matrix.md +63 -0
- data/docs/subsystems/midi.md +123 -0
- data/docs/subsystems/music.md +233 -0
- data/docs/subsystems/musicxml-builder.md +264 -0
- data/docs/subsystems/neumas.md +71 -0
- data/docs/subsystems/repl.md +135 -0
- data/docs/subsystems/sequencer.md +98 -0
- data/docs/subsystems/series.md +302 -0
- data/docs/subsystems/transcription.md +152 -0
- data/docs/subsystems/transport.md +177 -0
- data/lib/musa-dsl/core-ext/array-explode-ranges.rb +68 -0
- data/lib/musa-dsl/core-ext/arrayfy.rb +110 -0
- data/lib/musa-dsl/core-ext/attribute-builder.rb +91 -30
- data/lib/musa-dsl/core-ext/deep-copy.rb +125 -2
- data/lib/musa-dsl/core-ext/dynamic-proxy.rb +78 -0
- data/lib/musa-dsl/core-ext/extension.rb +53 -0
- data/lib/musa-dsl/core-ext/hashify.rb +162 -1
- data/lib/musa-dsl/core-ext/inspect-nice.rb +154 -0
- data/lib/musa-dsl/core-ext/smart-proc-binder.rb +117 -0
- data/lib/musa-dsl/core-ext/with.rb +114 -0
- data/lib/musa-dsl/datasets/dataset.rb +109 -0
- data/lib/musa-dsl/datasets/delta-d.rb +78 -0
- data/lib/musa-dsl/datasets/e.rb +186 -2
- data/lib/musa-dsl/datasets/gdv.rb +279 -2
- data/lib/musa-dsl/datasets/gdvd.rb +201 -0
- data/lib/musa-dsl/datasets/helper.rb +75 -0
- data/lib/musa-dsl/datasets/p.rb +177 -2
- data/lib/musa-dsl/datasets/packed-v.rb +91 -0
- data/lib/musa-dsl/datasets/pdv.rb +136 -1
- data/lib/musa-dsl/datasets/ps.rb +134 -4
- data/lib/musa-dsl/datasets/score/queriable.rb +143 -1
- data/lib/musa-dsl/datasets/score/render.rb +105 -1
- data/lib/musa-dsl/datasets/score/to-mxml/process-pdv.rb +138 -1
- data/lib/musa-dsl/datasets/score/to-mxml/process-ps.rb +111 -0
- data/lib/musa-dsl/datasets/score/to-mxml/process-time.rb +200 -1
- data/lib/musa-dsl/datasets/score/to-mxml/to-mxml.rb +145 -1
- data/lib/musa-dsl/datasets/score.rb +279 -0
- data/lib/musa-dsl/datasets/v.rb +88 -0
- data/lib/musa-dsl/generative/darwin.rb +180 -1
- data/lib/musa-dsl/generative/generative-grammar.rb +359 -0
- data/lib/musa-dsl/generative/markov.rb +133 -3
- data/lib/musa-dsl/generative/rules.rb +258 -4
- data/lib/musa-dsl/generative/variatio.rb +217 -2
- data/lib/musa-dsl/logger/logger.rb +267 -2
- data/lib/musa-dsl/matrix/matrix.rb +256 -10
- data/lib/musa-dsl/midi/midi-recorder.rb +108 -1
- data/lib/musa-dsl/midi/midi-voices.rb +265 -4
- data/lib/musa-dsl/music/chord-definition.rb +233 -1
- data/lib/musa-dsl/music/chord-definitions.rb +33 -6
- data/lib/musa-dsl/music/chords.rb +308 -2
- data/lib/musa-dsl/music/equally-tempered-12-tone-scale-system.rb +315 -0
- data/lib/musa-dsl/music/scales.rb +957 -40
- data/lib/musa-dsl/musicxml/builder/attributes.rb +483 -3
- data/lib/musa-dsl/musicxml/builder/backup-forward.rb +166 -1
- data/lib/musa-dsl/musicxml/builder/direction.rb +243 -0
- data/lib/musa-dsl/musicxml/builder/helper.rb +240 -0
- data/lib/musa-dsl/musicxml/builder/measure.rb +284 -0
- data/lib/musa-dsl/musicxml/builder/note-complexities.rb +324 -8
- data/lib/musa-dsl/musicxml/builder/note.rb +285 -0
- data/lib/musa-dsl/musicxml/builder/part-group.rb +108 -1
- data/lib/musa-dsl/musicxml/builder/part.rb +139 -0
- data/lib/musa-dsl/musicxml/builder/pitched-note.rb +124 -0
- data/lib/musa-dsl/musicxml/builder/rest.rb +93 -0
- data/lib/musa-dsl/musicxml/builder/score-partwise.rb +276 -0
- data/lib/musa-dsl/musicxml/builder/typed-text.rb +62 -1
- data/lib/musa-dsl/musicxml/builder/unpitched-note.rb +83 -0
- data/lib/musa-dsl/neumalang/neumalang.rb +675 -0
- data/lib/musa-dsl/neumas/array-to-neumas.rb +149 -0
- data/lib/musa-dsl/neumas/neuma-decoder.rb +253 -0
- data/lib/musa-dsl/neumas/neuma-gdv-decoder.rb +142 -2
- data/lib/musa-dsl/neumas/neuma-gdvd-decoder.rb +82 -0
- data/lib/musa-dsl/neumas/neumas.rb +67 -0
- data/lib/musa-dsl/neumas/string-to-neumas.rb +233 -1
- data/lib/musa-dsl/repl/repl.rb +550 -0
- data/lib/musa-dsl/sequencer/base-sequencer-implementation-every.rb +118 -2
- data/lib/musa-dsl/sequencer/base-sequencer-implementation-move.rb +149 -2
- data/lib/musa-dsl/sequencer/base-sequencer-implementation-play-helper.rb +296 -0
- data/lib/musa-dsl/sequencer/base-sequencer-implementation-play-timed.rb +88 -2
- data/lib/musa-dsl/sequencer/base-sequencer-implementation-play.rb +161 -0
- data/lib/musa-dsl/sequencer/base-sequencer-implementation.rb +263 -0
- data/lib/musa-dsl/sequencer/base-sequencer-tick-based.rb +173 -1
- data/lib/musa-dsl/sequencer/base-sequencer-tickless-based.rb +177 -0
- data/lib/musa-dsl/sequencer/base-sequencer.rb +710 -10
- data/lib/musa-dsl/sequencer/sequencer-dsl.rb +210 -0
- data/lib/musa-dsl/sequencer/timeslots.rb +79 -0
- data/lib/musa-dsl/series/array-to-serie.rb +37 -1
- data/lib/musa-dsl/series/base-series.rb +843 -5
- data/lib/musa-dsl/series/buffer-serie.rb +48 -0
- data/lib/musa-dsl/series/hash-or-array-serie-splitter.rb +41 -0
- data/lib/musa-dsl/series/main-serie-constructors.rb +398 -2
- data/lib/musa-dsl/series/main-serie-operations.rb +538 -16
- data/lib/musa-dsl/series/proxy-serie.rb +67 -0
- data/lib/musa-dsl/series/quantizer-serie.rb +45 -7
- data/lib/musa-dsl/series/queue-serie.rb +65 -0
- data/lib/musa-dsl/series/series-composer.rb +701 -0
- data/lib/musa-dsl/series/timed-serie.rb +473 -28
- data/lib/musa-dsl/transcription/from-gdv-to-midi.rb +404 -1
- data/lib/musa-dsl/transcription/from-gdv-to-musicxml.rb +118 -0
- data/lib/musa-dsl/transcription/from-gdv.rb +84 -1
- data/lib/musa-dsl/transcription/transcription.rb +265 -0
- data/lib/musa-dsl/transport/clock.rb +125 -0
- data/lib/musa-dsl/transport/dummy-clock.rb +89 -2
- data/lib/musa-dsl/transport/external-tick-clock.rb +91 -0
- data/lib/musa-dsl/transport/input-midi-clock.rb +133 -1
- data/lib/musa-dsl/transport/timer-clock.rb +183 -1
- data/lib/musa-dsl/transport/timer.rb +83 -0
- data/lib/musa-dsl/transport/transport.rb +318 -0
- data/lib/musa-dsl/version.rb +1 -1
- data/lib/musa-dsl.rb +132 -25
- data/musa-dsl.gemspec +12 -10
- metadata +87 -8
|
@@ -6,10 +6,83 @@ require_relative '../core-ext/arrayfy'
|
|
|
6
6
|
module Musa
|
|
7
7
|
module Series
|
|
8
8
|
module Operations
|
|
9
|
+
# Creates a composer transformation pipeline for complex multi-stage transformations.
|
|
10
|
+
#
|
|
11
|
+
# Composer provides declarative DSL for building transformation pipelines
|
|
12
|
+
# with multiple inputs, outputs, and intermediate processing stages.
|
|
13
|
+
#
|
|
14
|
+
# ## Composer Concepts
|
|
15
|
+
#
|
|
16
|
+
# - **Pipelines**: Named transformation chains
|
|
17
|
+
# - **Inputs**: Named input series (proxied and buffered)
|
|
18
|
+
# - **Outputs**: Named output series
|
|
19
|
+
# - **Operations**: Transformation steps in pipeline
|
|
20
|
+
# - **Auto-commit**: Automatic finalization of pipelines
|
|
21
|
+
#
|
|
22
|
+
# ## DSL Structure
|
|
23
|
+
#
|
|
24
|
+
# ```ruby
|
|
25
|
+
# composer do
|
|
26
|
+
# input_name >> operation1 >> operation2 >> :output_name
|
|
27
|
+
# :other_input >> transform >> :other_output
|
|
28
|
+
# end
|
|
29
|
+
# ```
|
|
30
|
+
#
|
|
31
|
+
# ## Musical Applications
|
|
32
|
+
#
|
|
33
|
+
# - Complex multi-voice processing
|
|
34
|
+
# - Effect chains and routing
|
|
35
|
+
# - Algorithmic composition pipelines
|
|
36
|
+
# - Multi-stage transformations
|
|
37
|
+
# - Modular synthesis-style routing
|
|
38
|
+
#
|
|
39
|
+
# @example Basic composer
|
|
40
|
+
# s = S(1, 2, 3).composer do
|
|
41
|
+
# input.map { |x| x * 2 } >> :output
|
|
42
|
+
# end
|
|
43
|
+
# s.i.to_a # => [2, 4, 6]
|
|
44
|
+
#
|
|
45
|
+
# @example Multi-pipeline
|
|
46
|
+
# composer = Composer.new(input: S(1, 2, 3)) do
|
|
47
|
+
# input.map { |x| x * 2 } >> :doubled
|
|
48
|
+
# input.map { |x| x * 3 } >> :tripled
|
|
49
|
+
# end
|
|
50
|
+
# composer.output(:doubled).i.to_a # => [2, 4, 6]
|
|
51
|
+
# composer.output(:tripled).i.to_a # => [3, 6, 9]
|
|
52
|
+
#
|
|
53
|
+
# @yield composer DSL block
|
|
54
|
+
#
|
|
55
|
+
# @return [ComposerAsOperationSerie] composed serie
|
|
56
|
+
#
|
|
57
|
+
# @example Simple transformation
|
|
58
|
+
# s.composer do
|
|
59
|
+
# input.map { |x| x + 1 } >> :output
|
|
60
|
+
# end
|
|
61
|
+
#
|
|
62
|
+
# @api public
|
|
9
63
|
def composer(&block)
|
|
10
64
|
ComposerAsOperationSerie.new(self, &block)
|
|
11
65
|
end
|
|
12
66
|
|
|
67
|
+
# Wrapper that exposes Composer as a serie operation.
|
|
68
|
+
#
|
|
69
|
+
# Allows {Composer::Composer} pipelines to be used as standard serie
|
|
70
|
+
# operations. The composer takes input from the source serie, processes
|
|
71
|
+
# it through the configured pipeline, and outputs transformed values.
|
|
72
|
+
#
|
|
73
|
+
# This enables complex multi-input/output serie transformations to
|
|
74
|
+
# be used in serie operation chains.
|
|
75
|
+
#
|
|
76
|
+
# @example Using composer as operation
|
|
77
|
+
# serie = FromArray.new([1, 2, 3, 4])
|
|
78
|
+
# transformed = serie.composer do
|
|
79
|
+
# # Composer pipeline configuration
|
|
80
|
+
# pipeline { |v| v * 2 }
|
|
81
|
+
# end
|
|
82
|
+
# transformed.next_value # => 2
|
|
83
|
+
#
|
|
84
|
+
# @see Composer::Composer Full composer implementation
|
|
85
|
+
# @api private
|
|
13
86
|
class ComposerAsOperationSerie
|
|
14
87
|
include Musa::Series::Serie.with(source: true)
|
|
15
88
|
|
|
@@ -37,7 +110,177 @@ module Musa
|
|
|
37
110
|
end
|
|
38
111
|
end
|
|
39
112
|
|
|
113
|
+
# Series composition and transformation pipeline system.
|
|
114
|
+
#
|
|
115
|
+
# Provides infrastructure for building complex multi-input/output
|
|
116
|
+
# transformation pipelines with named routes, intermediate stages,
|
|
117
|
+
# and declarative DSL syntax.
|
|
118
|
+
#
|
|
119
|
+
# The Composer system enables modular, synthesis-style routing of
|
|
120
|
+
# serie transformations with explicit input/output management.
|
|
121
|
+
#
|
|
122
|
+
# @see Composer::Composer Main composer implementation
|
|
123
|
+
# @see ComposerAsOperationSerie Composer as serie operation
|
|
40
124
|
module Composer
|
|
125
|
+
# Multi-input/output serie transformation pipeline system.
|
|
126
|
+
#
|
|
127
|
+
# Composer enables building complex transformation graphs with multiple named
|
|
128
|
+
# inputs, outputs, and intermediate processing stages. It provides a declarative
|
|
129
|
+
# DSL for defining pipelines and routing data between them, similar to modular
|
|
130
|
+
# synthesis patch routing.
|
|
131
|
+
#
|
|
132
|
+
# ## Architecture
|
|
133
|
+
#
|
|
134
|
+
# The Composer system consists of:
|
|
135
|
+
#
|
|
136
|
+
# - **Pipelines**: Named transformation stages that process series
|
|
137
|
+
# - **Routes**: Connections between pipelines specifying data flow
|
|
138
|
+
# - **Inputs**: Named entry points (automatically proxied and buffered)
|
|
139
|
+
# - **Outputs**: Named exit points for consuming results
|
|
140
|
+
# - **DSL Context**: method_missing-based interface for pipeline definition
|
|
141
|
+
#
|
|
142
|
+
# ## Pipeline Definition
|
|
143
|
+
#
|
|
144
|
+
# Pipelines are defined using method calls in the DSL block. Each pipeline
|
|
145
|
+
# consists of:
|
|
146
|
+
#
|
|
147
|
+
# - **Constructor** (optional): Series constructor like `S`, `H`, etc.
|
|
148
|
+
# - **Operations** (one or more): Transformations like `reverse`, `skip`, etc.
|
|
149
|
+
#
|
|
150
|
+
# @example Pipeline with constructor
|
|
151
|
+
# composer = Composer.new(inputs: nil) do
|
|
152
|
+
# my_pipeline ({ S: [1, 2, 3] }), reverse, { skip: 1 }
|
|
153
|
+
# route my_pipeline, to: output
|
|
154
|
+
# end
|
|
155
|
+
# composer.output.i.to_a # => [2, 1]
|
|
156
|
+
# # Creates: S([1,2,3]) → reverse → skip(1)
|
|
157
|
+
#
|
|
158
|
+
# @example Pipeline with operations only
|
|
159
|
+
# composer = Composer.new(input: S(1, 2, 3)) do
|
|
160
|
+
# my_pipeline reverse, { skip: 1 }
|
|
161
|
+
# route input, to: my_pipeline
|
|
162
|
+
# route my_pipeline, to: output
|
|
163
|
+
# end
|
|
164
|
+
# composer.output.i.to_a # => [2, 1]
|
|
165
|
+
# # Applies: input → reverse → skip(1)
|
|
166
|
+
#
|
|
167
|
+
# ## Routing System
|
|
168
|
+
#
|
|
169
|
+
# Routes connect pipelines using the `route` method:
|
|
170
|
+
#
|
|
171
|
+
# ```ruby
|
|
172
|
+
# route from_pipeline, to: to_pipeline, on: :source, as: :key
|
|
173
|
+
# ```
|
|
174
|
+
#
|
|
175
|
+
# **Routing modes:**
|
|
176
|
+
#
|
|
177
|
+
# 1. **Hash assignment** (when `as:` provided):
|
|
178
|
+
#
|
|
179
|
+
# - Data is assigned to: `to_pipeline.input[on][as] = from_pipeline.output`
|
|
180
|
+
# - Used by operations accepting hash inputs (e.g., `H` constructor)
|
|
181
|
+
# - Default `on` is `:sources` when `as` is provided
|
|
182
|
+
#
|
|
183
|
+
# 2. **Setter method** (when `as:` omitted):
|
|
184
|
+
#
|
|
185
|
+
# - Calls: `to_pipeline.input.source = from_pipeline.output`
|
|
186
|
+
# - Used by operations with explicit source setters
|
|
187
|
+
# - Default `on` is `:source` when `as` is omitted
|
|
188
|
+
#
|
|
189
|
+
# @example Hash routing
|
|
190
|
+
# composer = Composer.new(inputs: [:a, :b], auto_commit: false) do
|
|
191
|
+
# step1 reverse
|
|
192
|
+
# step2 reverse
|
|
193
|
+
# hash_merge ({ H: {} })
|
|
194
|
+
#
|
|
195
|
+
# route a, to: step1
|
|
196
|
+
# route b, to: step2
|
|
197
|
+
# route step1, to: hash_merge, as: :x # hash_merge.input[:sources][:x] = step1.output
|
|
198
|
+
# route step2, to: hash_merge, as: :y # hash_merge.input[:sources][:y] = step2.output
|
|
199
|
+
# route hash_merge, to: output
|
|
200
|
+
# end
|
|
201
|
+
#
|
|
202
|
+
# composer.input(:a).proxy_source = S(1, 2, 3)
|
|
203
|
+
# composer.input(:b).proxy_source = S(10, 20, 30)
|
|
204
|
+
# composer.commit!
|
|
205
|
+
# composer.output.i.to_a # => [{x: 3, y: 30}, {x: 2, y: 20}, {x: 1, y: 10}]
|
|
206
|
+
#
|
|
207
|
+
# @example Setter routing
|
|
208
|
+
# composer = Composer.new(input: S(1, 2, 3)) do
|
|
209
|
+
# step1 reverse
|
|
210
|
+
# route input, to: step1 # step1.input.source = input.output
|
|
211
|
+
# route step1, to: output
|
|
212
|
+
# end
|
|
213
|
+
#
|
|
214
|
+
# composer.output.i.to_a # => [3, 2, 1]
|
|
215
|
+
#
|
|
216
|
+
# ## Commit System
|
|
217
|
+
#
|
|
218
|
+
# Composer uses two-phase initialization:
|
|
219
|
+
#
|
|
220
|
+
# 1. **Definition phase**: Routes and pipelines are declared
|
|
221
|
+
# 2. **Commit phase**: All connections are resolved and finalized
|
|
222
|
+
#
|
|
223
|
+
# **Key behaviors:**
|
|
224
|
+
# - Routes can be defined in any order (order-independent)
|
|
225
|
+
# - Output access is blocked until `commit!` is called
|
|
226
|
+
# - Commit resolves all routes and sets up buffering
|
|
227
|
+
# - `auto_commit: true` (default) commits automatically after DSL block
|
|
228
|
+
#
|
|
229
|
+
# ## DSL Context
|
|
230
|
+
#
|
|
231
|
+
# The DSL uses `method_missing` to enable natural pipeline definition:
|
|
232
|
+
#
|
|
233
|
+
# - **Named access**: `input`, `output`, pipeline names return symbols
|
|
234
|
+
# - **Pipeline creation**: `name arg1, arg2, ...` creates pipeline
|
|
235
|
+
# - **Operation symbols**: Series operations return as symbols for parsing
|
|
236
|
+
# - **Constructor symbols**: Series constructors return as symbols for parsing
|
|
237
|
+
#
|
|
238
|
+
# @example Basic pipeline
|
|
239
|
+
# composer = Composer.new(input: S(1, 2, 3)) do
|
|
240
|
+
# step1 reverse
|
|
241
|
+
# route input, to: step1
|
|
242
|
+
# route step1, to: output
|
|
243
|
+
# end
|
|
244
|
+
# composer.output.i.to_a # => [3, 2, 1]
|
|
245
|
+
#
|
|
246
|
+
# @example Multiple inputs merging
|
|
247
|
+
# composer = Composer.new(inputs: { a: S(1, 2), b: S(10, 20) }) do
|
|
248
|
+
# hash_merge ({ H: {} })
|
|
249
|
+
# route a, to: hash_merge, as: :x
|
|
250
|
+
# route b, to: hash_merge, as: :y
|
|
251
|
+
# route hash_merge, to: output
|
|
252
|
+
# end
|
|
253
|
+
# composer.output.i.to_a # => [{x: 1, y: 10}, {x: 2, y: 20}]
|
|
254
|
+
#
|
|
255
|
+
# @example Multiple outputs
|
|
256
|
+
# composer = Composer.new(input: S(1, 2, 3)) do
|
|
257
|
+
# doubled ({ eval: ->(v) { v * 2 } })
|
|
258
|
+
# tripled ({ eval: ->(v) { v * 3 } })
|
|
259
|
+
#
|
|
260
|
+
# route input, to: doubled
|
|
261
|
+
# route input, to: tripled
|
|
262
|
+
# route doubled, to: output
|
|
263
|
+
# end
|
|
264
|
+
# composer.output.i.to_a # => [2, 4, 6]
|
|
265
|
+
#
|
|
266
|
+
# @example Complex routing
|
|
267
|
+
# composer = Composer.new(inputs: [:a, :b], auto_commit: false) do
|
|
268
|
+
# step1 reverse
|
|
269
|
+
# step2 ({ skip: 1 })
|
|
270
|
+
# hash_merge ({ H: {} })
|
|
271
|
+
#
|
|
272
|
+
# route a, to: step1
|
|
273
|
+
# route b, to: step2
|
|
274
|
+
# route step1, to: hash_merge, as: :x
|
|
275
|
+
# route step2, to: hash_merge, as: :y
|
|
276
|
+
# route hash_merge, to: output
|
|
277
|
+
# end
|
|
278
|
+
#
|
|
279
|
+
# composer.input(:a).proxy_source = S(1, 2, 3)
|
|
280
|
+
# composer.input(:b).proxy_source = S(10, 20, 30)
|
|
281
|
+
# composer.commit!
|
|
282
|
+
#
|
|
283
|
+
# composer.output.i.to_a # => [{x: 3, y: 20}, {x: 2, y: 30}]
|
|
41
284
|
class Composer
|
|
42
285
|
using Musa::Extension::Arrayfy
|
|
43
286
|
|
|
@@ -87,11 +330,52 @@ module Musa
|
|
|
87
330
|
commit! if auto_commit
|
|
88
331
|
end
|
|
89
332
|
|
|
333
|
+
# Accesses named input proxy for dynamic source assignment.
|
|
334
|
+
#
|
|
335
|
+
# Returns the proxy series for the specified input, allowing dynamic
|
|
336
|
+
# assignment of source series after composer creation. Used with
|
|
337
|
+
# `auto_commit: false` to set sources before manual commit.
|
|
338
|
+
#
|
|
339
|
+
# @param name [Symbol, nil] input name (defaults to :input)
|
|
340
|
+
#
|
|
341
|
+
# @return [ProxySerie] proxy series for source assignment
|
|
342
|
+
#
|
|
343
|
+
# @example Set input source dynamically
|
|
344
|
+
# composer = Composer.new(auto_commit: false) do
|
|
345
|
+
# step reverse
|
|
346
|
+
# route input, to: step
|
|
347
|
+
# route step, to: output
|
|
348
|
+
# end
|
|
349
|
+
#
|
|
350
|
+
# composer.input.proxy_source = S(1, 2, 3)
|
|
351
|
+
# composer.commit!
|
|
352
|
+
#
|
|
353
|
+
# @api public
|
|
90
354
|
def input(name = nil)
|
|
91
355
|
name ||= :input
|
|
92
356
|
@inputs[name].input
|
|
93
357
|
end
|
|
94
358
|
|
|
359
|
+
# Accesses named output series for consumption.
|
|
360
|
+
#
|
|
361
|
+
# Returns the output series for the specified output. Can only be
|
|
362
|
+
# called after `commit!` has been invoked. Raises error if composer
|
|
363
|
+
# is not committed.
|
|
364
|
+
#
|
|
365
|
+
# @param name [Symbol, nil] output name (defaults to :output)
|
|
366
|
+
#
|
|
367
|
+
# @return [Serie] output series
|
|
368
|
+
#
|
|
369
|
+
# @raise [RuntimeError] if composer not yet committed
|
|
370
|
+
#
|
|
371
|
+
# @example Access output
|
|
372
|
+
# composer.output.i.to_a # => [3, 2, 1]
|
|
373
|
+
#
|
|
374
|
+
# @example Multiple outputs
|
|
375
|
+
# composer.output(:doubled).i.to_a # => [2, 4, 6]
|
|
376
|
+
# composer.output(:tripled).i.to_a # => [3, 6, 9]
|
|
377
|
+
#
|
|
378
|
+
# @api public
|
|
95
379
|
def output(name = nil)
|
|
96
380
|
raise "Can't access output if the Composer is uncommited. Call '.commit' first." unless @commited
|
|
97
381
|
|
|
@@ -99,18 +383,152 @@ module Musa
|
|
|
99
383
|
@outputs[name].output
|
|
100
384
|
end
|
|
101
385
|
|
|
386
|
+
# Defines routing connection between pipelines.
|
|
387
|
+
#
|
|
388
|
+
# Creates data flow route from source pipeline to destination pipeline.
|
|
389
|
+
# The routing behavior depends on whether `as:` parameter is provided.
|
|
390
|
+
#
|
|
391
|
+
# **With `as:` parameter (Hash routing):**
|
|
392
|
+
# - Assigns to hash: `to_pipeline.input[on][as] = from_pipeline.output`
|
|
393
|
+
# - Default `on` is `:sources`
|
|
394
|
+
# - Used by operations accepting hash inputs (e.g., `H` constructor)
|
|
395
|
+
#
|
|
396
|
+
# **Without `as:` parameter (Setter routing):**
|
|
397
|
+
# - Calls setter: `to_pipeline.input.source = from_pipeline.output`
|
|
398
|
+
# - Default `on` is `:source`
|
|
399
|
+
# - Used by operations with explicit source setter methods
|
|
400
|
+
#
|
|
401
|
+
# @param from [Symbol] source pipeline name
|
|
402
|
+
# @param to [Symbol] destination pipeline name
|
|
403
|
+
# @param on [Symbol, nil] attribute name (:source or :sources)
|
|
404
|
+
# @param as [Symbol, nil] hash key for assignment
|
|
405
|
+
#
|
|
406
|
+
# @return [void]
|
|
407
|
+
#
|
|
408
|
+
# @raise [ArgumentError] if pipeline names not found
|
|
409
|
+
# @raise [ArgumentError] if route already exists
|
|
410
|
+
#
|
|
411
|
+
# @example Hash routing (inside DSL block)
|
|
412
|
+
# composer = Composer.new(inputs: [:a, :b], auto_commit: false) do
|
|
413
|
+
# step1 reverse
|
|
414
|
+
# step2 reverse
|
|
415
|
+
# hash_merge ({ H: {} })
|
|
416
|
+
#
|
|
417
|
+
# route a, to: step1
|
|
418
|
+
# route b, to: step2
|
|
419
|
+
# route step1, to: hash_merge, as: :x # hash_merge.input[:sources][:x] = step1
|
|
420
|
+
# route step2, to: hash_merge, as: :y # hash_merge.input[:sources][:y] = step2
|
|
421
|
+
# route hash_merge, to: output
|
|
422
|
+
# end
|
|
423
|
+
#
|
|
424
|
+
# composer.input(:a).proxy_source = S(1, 2)
|
|
425
|
+
# composer.input(:b).proxy_source = S(10, 20)
|
|
426
|
+
# composer.commit!
|
|
427
|
+
# composer.output.i.to_a # => [{x: 2, y: 20}, {x: 1, y: 10}]
|
|
428
|
+
#
|
|
429
|
+
# @example Setter routing (inside DSL block)
|
|
430
|
+
# composer = Composer.new(input: S(1, 2, 3)) do
|
|
431
|
+
# step reverse
|
|
432
|
+
# route input, to: step # step.input.source = input
|
|
433
|
+
# route step, to: output
|
|
434
|
+
# end
|
|
435
|
+
#
|
|
436
|
+
# composer.output.i.to_a # => [3, 2, 1]
|
|
437
|
+
#
|
|
438
|
+
# @example Custom on parameter (inside DSL block)
|
|
439
|
+
# composer = Composer.new(input: S(1, 2, 3), auto_commit: false) do
|
|
440
|
+
# step reverse
|
|
441
|
+
# hash_merge ({ H: {} })
|
|
442
|
+
# route input, to: step
|
|
443
|
+
# route step, to: hash_merge, on: :sources, as: :x
|
|
444
|
+
# route hash_merge, to: output
|
|
445
|
+
# end
|
|
446
|
+
#
|
|
447
|
+
# composer.commit!
|
|
448
|
+
# composer.output.i.to_a # => [{x: 3}, {x: 2}, {x: 1}]
|
|
449
|
+
#
|
|
450
|
+
# @api public
|
|
102
451
|
def route(from, to:, on: nil, as: nil)
|
|
103
452
|
@dsl.route(from, to: to, on: on, as: as)
|
|
104
453
|
end
|
|
105
454
|
|
|
455
|
+
# Defines named pipeline with transformation operations.
|
|
456
|
+
#
|
|
457
|
+
# Creates a pipeline from constructor and/or operations. This method
|
|
458
|
+
# is typically called implicitly through the DSL's method_missing, but
|
|
459
|
+
# can be called directly for dynamic pipeline creation.
|
|
460
|
+
#
|
|
461
|
+
# @param name [Symbol] pipeline name
|
|
462
|
+
# @param elements [Array] constructor and operations
|
|
463
|
+
#
|
|
464
|
+
# @return [void]
|
|
465
|
+
#
|
|
466
|
+
# @example Direct call (inside DSL block)
|
|
467
|
+
# composer = Composer.new(inputs: nil) do
|
|
468
|
+
# pipeline(:my_step, [{ S: [1, 2, 3] }, :reverse])
|
|
469
|
+
# route my_step, to: output
|
|
470
|
+
# end
|
|
471
|
+
#
|
|
472
|
+
# composer.output.i.to_a # => [3, 2, 1]
|
|
473
|
+
#
|
|
474
|
+
# @example DSL equivalent (method_missing)
|
|
475
|
+
# composer = Composer.new(inputs: nil) do
|
|
476
|
+
# my_step ({ S: [1, 2, 3] }), reverse
|
|
477
|
+
# route my_step, to: output
|
|
478
|
+
# end
|
|
479
|
+
#
|
|
480
|
+
# composer.output.i.to_a # => [3, 2, 1]
|
|
481
|
+
#
|
|
482
|
+
# @api public
|
|
106
483
|
def pipeline(name, *elements)
|
|
107
484
|
@dsl.pipeline(name, elements)
|
|
108
485
|
end
|
|
109
486
|
|
|
487
|
+
# Updates composer with additional DSL block.
|
|
488
|
+
#
|
|
489
|
+
# Allows dynamic modification of composer after creation by executing
|
|
490
|
+
# additional DSL block in the composer's DSL context. Useful for
|
|
491
|
+
# progressive pipeline construction.
|
|
492
|
+
#
|
|
493
|
+
# @yield DSL block with additional pipeline definitions
|
|
494
|
+
#
|
|
495
|
+
# @return [void]
|
|
496
|
+
#
|
|
497
|
+
# @example Add routes dynamically
|
|
498
|
+
# composer.update do
|
|
499
|
+
# route step3, to: output
|
|
500
|
+
# end
|
|
501
|
+
#
|
|
502
|
+
# @api public
|
|
110
503
|
def update(&block)
|
|
111
504
|
@dsl.with &block
|
|
112
505
|
end
|
|
113
506
|
|
|
507
|
+
# Finalizes composer by resolving all routes and connections.
|
|
508
|
+
#
|
|
509
|
+
# Commits the composer, resolving all route connections and setting up
|
|
510
|
+
# buffering. Must be called before accessing outputs. Cannot be called
|
|
511
|
+
# twice on same composer instance.
|
|
512
|
+
#
|
|
513
|
+
# The commit process:
|
|
514
|
+
# 1. Recursively commits all output pipelines
|
|
515
|
+
# 2. Each pipeline commits its input routes
|
|
516
|
+
# 3. Connects all sources through buffers
|
|
517
|
+
# 4. Sets committed flag enabling output access
|
|
518
|
+
#
|
|
519
|
+
# @return [void]
|
|
520
|
+
#
|
|
521
|
+
# @raise [RuntimeError] if already committed
|
|
522
|
+
#
|
|
523
|
+
# @example Manual commit
|
|
524
|
+
# composer = Composer.new(auto_commit: false) do
|
|
525
|
+
# # ... pipeline definitions ...
|
|
526
|
+
# end
|
|
527
|
+
# composer.input.proxy_source = S(1, 2, 3)
|
|
528
|
+
# composer.commit!
|
|
529
|
+
# result = composer.output.i.to_a
|
|
530
|
+
#
|
|
531
|
+
# @api public
|
|
114
532
|
def commit!
|
|
115
533
|
raise 'Already commited' if @commited
|
|
116
534
|
|
|
@@ -121,7 +539,38 @@ module Musa
|
|
|
121
539
|
@commited = true
|
|
122
540
|
end
|
|
123
541
|
|
|
542
|
+
# Internal representation of a transformation pipeline stage.
|
|
543
|
+
#
|
|
544
|
+
# Pipeline encapsulates a single named stage in the composer graph,
|
|
545
|
+
# storing its input/output series, routes, and transformation logic.
|
|
546
|
+
#
|
|
547
|
+
# ## Pipeline Types
|
|
548
|
+
#
|
|
549
|
+
# - **Input pipelines**: Created from `inputs:` parameter, wrap proxy series
|
|
550
|
+
# - **Output pipelines**: Created from `outputs:` parameter, wrap proxy series
|
|
551
|
+
# - **Transformation pipelines**: Created by DSL, contain transformation logic
|
|
552
|
+
#
|
|
553
|
+
# ## Pipeline Components
|
|
554
|
+
#
|
|
555
|
+
# - `@first_proc`: Proc creating initial series from UNDEFINED (constructors)
|
|
556
|
+
# - `@chain_proc`: Proc applying operations to existing series
|
|
557
|
+
# - `@routes`: Hash of incoming route connections
|
|
558
|
+
# - `@input`: Input series (set during commit)
|
|
559
|
+
# - `@output`: Output series (set during commit)
|
|
560
|
+
#
|
|
561
|
+
# @api private
|
|
124
562
|
class Pipeline
|
|
563
|
+
# Creates new pipeline stage.
|
|
564
|
+
#
|
|
565
|
+
# @param name [Symbol] pipeline name
|
|
566
|
+
# @param is_output [Boolean] whether this is an output pipeline
|
|
567
|
+
# @param input [Serie, nil] input series
|
|
568
|
+
# @param output [Serie, nil] output series
|
|
569
|
+
# @param first_proc [Proc, nil] constructor proc
|
|
570
|
+
# @param chain_proc [Proc, nil] operations proc
|
|
571
|
+
# @param pipelines [Hash] reference to all pipelines
|
|
572
|
+
#
|
|
573
|
+
# @api private
|
|
125
574
|
def initialize(name, is_output: false, input: nil, output: nil, first_proc: nil, chain_proc: nil, pipelines:)
|
|
126
575
|
@name = name
|
|
127
576
|
@is_output = is_output
|
|
@@ -136,14 +585,46 @@ module Musa
|
|
|
136
585
|
attr_reader :name, :is_output
|
|
137
586
|
attr_accessor :input, :output, :proc
|
|
138
587
|
|
|
588
|
+
# Retrieves route at specified connection point.
|
|
589
|
+
#
|
|
590
|
+
# @param on [Symbol] connection attribute name
|
|
591
|
+
# @param as [Symbol, nil] hash key for assignment
|
|
592
|
+
#
|
|
593
|
+
# @return [Route, nil] route at connection point
|
|
594
|
+
#
|
|
595
|
+
# @api private
|
|
139
596
|
def [](on, as)
|
|
140
597
|
@routes[[on, as]]
|
|
141
598
|
end
|
|
142
599
|
|
|
600
|
+
# Stores route at specified connection point.
|
|
601
|
+
#
|
|
602
|
+
# @param on [Symbol] connection attribute name
|
|
603
|
+
# @param as [Symbol, nil] hash key for assignment
|
|
604
|
+
# @param source [Pipeline] source pipeline
|
|
605
|
+
#
|
|
606
|
+
# @return [Route] created route
|
|
607
|
+
#
|
|
608
|
+
# @api private
|
|
143
609
|
def []=(on, as, source)
|
|
144
610
|
@routes[[on, as]] = Route.new(on, as, source)
|
|
145
611
|
end
|
|
146
612
|
|
|
613
|
+
# Finalizes pipeline by resolving routes and connecting series.
|
|
614
|
+
#
|
|
615
|
+
# The commit process:
|
|
616
|
+
# 1. Calls `@first_proc` with UNDEFINED to create initial series (if present)
|
|
617
|
+
# 2. Recursively commits all source pipelines
|
|
618
|
+
# 3. Connects input routes:
|
|
619
|
+
# - For output pipelines: assigns to proxy_source
|
|
620
|
+
# - For hash routes (with `as`): assigns to input[on][as]
|
|
621
|
+
# - For setter routes (without `as`): calls input.on=
|
|
622
|
+
# 4. Applies `@chain_proc` to transform input series (if present)
|
|
623
|
+
# 5. Sets output series (buffered)
|
|
624
|
+
#
|
|
625
|
+
# @return [self]
|
|
626
|
+
#
|
|
627
|
+
# @api private
|
|
147
628
|
def commit!
|
|
148
629
|
first_serie_operation = @first_proc&.call(Musa::Series::Constructors.UNDEFINED())
|
|
149
630
|
|
|
@@ -168,7 +649,20 @@ module Musa
|
|
|
168
649
|
end
|
|
169
650
|
end
|
|
170
651
|
|
|
652
|
+
# Route connection between two pipelines.
|
|
653
|
+
#
|
|
654
|
+
# Stores the metadata for a single route connection, including the
|
|
655
|
+
# connection point (on), hash key (as), and source pipeline.
|
|
656
|
+
#
|
|
657
|
+
# @api private
|
|
171
658
|
class Route
|
|
659
|
+
# Creates new route.
|
|
660
|
+
#
|
|
661
|
+
# @param on [Symbol] connection attribute name
|
|
662
|
+
# @param as [Symbol, nil] hash key for assignment
|
|
663
|
+
# @param source [Pipeline] source pipeline
|
|
664
|
+
#
|
|
665
|
+
# @api private
|
|
172
666
|
def initialize(on, as, source)
|
|
173
667
|
@on = on
|
|
174
668
|
@as = as
|
|
@@ -177,13 +671,56 @@ module Musa
|
|
|
177
671
|
attr_accessor :on, :as, :source
|
|
178
672
|
end
|
|
179
673
|
|
|
674
|
+
# DSL execution context for pipeline definition.
|
|
675
|
+
#
|
|
676
|
+
# DSLContext provides the execution environment for the composer DSL block.
|
|
677
|
+
# It uses `method_missing` to enable natural syntax for pipeline definition
|
|
678
|
+
# and routing operations.
|
|
679
|
+
#
|
|
680
|
+
# ## Method Resolution
|
|
681
|
+
#
|
|
682
|
+
# - **Series operations** (`reverse`, `skip`, etc.): Return symbol for parsing
|
|
683
|
+
# - **Series constructors** (`S`, `H`, etc.): Return symbol for parsing
|
|
684
|
+
# - **Pipeline names**: Return symbol for routing
|
|
685
|
+
# - **With arguments**: Create pipeline with those arguments
|
|
686
|
+
#
|
|
687
|
+
# ## Key Responsibilities
|
|
688
|
+
#
|
|
689
|
+
# - Parse pipeline definitions into first/chain procs
|
|
690
|
+
# - Validate and store route connections
|
|
691
|
+
# - Distinguish between constructors and operations
|
|
692
|
+
# - Handle dynamic parameter passing
|
|
693
|
+
#
|
|
694
|
+
# @api private
|
|
180
695
|
class DSLContext
|
|
181
696
|
include Musa::Extension::With
|
|
182
697
|
|
|
698
|
+
# Creates DSL context.
|
|
699
|
+
#
|
|
700
|
+
# @param pipelines [Hash] reference to all pipelines
|
|
701
|
+
#
|
|
702
|
+
# @api private
|
|
183
703
|
def initialize(pipelines)
|
|
184
704
|
@pipelines = pipelines
|
|
185
705
|
end
|
|
186
706
|
|
|
707
|
+
# Defines route connection in DSL context.
|
|
708
|
+
#
|
|
709
|
+
# Validates pipeline existence, determines default `on` parameter,
|
|
710
|
+
# checks for duplicate routes, and stores route in destination pipeline.
|
|
711
|
+
#
|
|
712
|
+
# @param from [Symbol] source pipeline name
|
|
713
|
+
# @param to [Symbol] destination pipeline name
|
|
714
|
+
# @param on [Symbol, nil] connection attribute
|
|
715
|
+
# @param as [Symbol, nil] hash key
|
|
716
|
+
#
|
|
717
|
+
# @return [void]
|
|
718
|
+
#
|
|
719
|
+
# @raise [ArgumentError] if pipelines not found
|
|
720
|
+
# @raise [ArgumentError] if route already exists
|
|
721
|
+
# @raise [ArgumentError] if output pipeline with on/as parameters
|
|
722
|
+
#
|
|
723
|
+
# @api private
|
|
187
724
|
def route(from, to:, on: nil, as: nil)
|
|
188
725
|
from_pipeline = @pipelines[from]
|
|
189
726
|
to_pipeline = @pipelines[to]
|
|
@@ -195,6 +732,7 @@ module Musa
|
|
|
195
732
|
raise ArgumentError, "Output pipeline #{to_pipeline.name} only allows default routing"
|
|
196
733
|
end
|
|
197
734
|
|
|
735
|
+
# Default on logic: :sources when as provided, :source otherwise
|
|
198
736
|
on ||= (as ? :sources : :source)
|
|
199
737
|
|
|
200
738
|
raise ArgumentError,
|
|
@@ -205,6 +743,17 @@ module Musa
|
|
|
205
743
|
to_pipeline[on, as] = from_pipeline
|
|
206
744
|
end
|
|
207
745
|
|
|
746
|
+
# Creates pipeline from elements in DSL context.
|
|
747
|
+
#
|
|
748
|
+
# Parses elements into first/chain procs, creates Pipeline instance,
|
|
749
|
+
# and defines DSL accessor method for pipeline name.
|
|
750
|
+
#
|
|
751
|
+
# @param name [Symbol] pipeline name
|
|
752
|
+
# @param elements [Array] constructor and/or operations
|
|
753
|
+
#
|
|
754
|
+
# @return [void]
|
|
755
|
+
#
|
|
756
|
+
# @api private
|
|
208
757
|
def pipeline(name, elements)
|
|
209
758
|
first, chain = parse(elements)
|
|
210
759
|
@pipelines[name] = Pipeline.new(name, first_proc: first, chain_proc: chain, pipelines: @pipelines)
|
|
@@ -212,6 +761,41 @@ module Musa
|
|
|
212
761
|
define_singleton_method(name) { name }
|
|
213
762
|
end
|
|
214
763
|
|
|
764
|
+
# Parses pipeline elements into first/chain proc pair.
|
|
765
|
+
#
|
|
766
|
+
# The parser transforms DSL syntax into executable procs. It splits
|
|
767
|
+
# pipeline definition into:
|
|
768
|
+
# - `first`: Proc creating initial series from UNDEFINED (constructors)
|
|
769
|
+
# - `chain`: Proc applying operations to existing series
|
|
770
|
+
#
|
|
771
|
+
# **Parsing modes:**
|
|
772
|
+
#
|
|
773
|
+
# - **Array**: Sequence of operations/constructors
|
|
774
|
+
# - **Hash**: Single operation/constructor with parameters
|
|
775
|
+
# - **Symbol**: Operation/constructor name without parameters
|
|
776
|
+
# - **Proc**: Direct transformation function
|
|
777
|
+
#
|
|
778
|
+
# **First vs Chain logic:**
|
|
779
|
+
# - First element (if constructor) becomes `first`
|
|
780
|
+
# - Remaining elements compose into `chain`
|
|
781
|
+
# - Operations only: `first` is nil, all become `chain`
|
|
782
|
+
#
|
|
783
|
+
# @param thing [Array, Hash, Symbol, Proc, Object] element(s) to parse
|
|
784
|
+
#
|
|
785
|
+
# @return [Array(Proc, Proc), Proc, Object] [first, chain] pair or single proc
|
|
786
|
+
#
|
|
787
|
+
# @example Array with constructor + operations
|
|
788
|
+
# parse([{ S: [1, 2, 3] }, :reverse, { skip: 1 }])
|
|
789
|
+
# # => [first_proc, chain_proc]
|
|
790
|
+
# # first_proc: creates S([1,2,3])
|
|
791
|
+
# # chain_proc: applies reverse >> skip(1)
|
|
792
|
+
#
|
|
793
|
+
# @example Operations only
|
|
794
|
+
# parse([:reverse, { skip: 1 }])
|
|
795
|
+
# # => [nil, chain_proc]
|
|
796
|
+
# # chain_proc: applies reverse >> skip(1)
|
|
797
|
+
#
|
|
798
|
+
# @api private
|
|
215
799
|
private def parse(thing)
|
|
216
800
|
case thing
|
|
217
801
|
when Array
|
|
@@ -263,6 +847,18 @@ module Musa
|
|
|
263
847
|
end
|
|
264
848
|
end
|
|
265
849
|
|
|
850
|
+
# Wraps operation/constructor as chainable proc.
|
|
851
|
+
#
|
|
852
|
+
# Creates proc that accepts previous result (`last`) and applies
|
|
853
|
+
# operation/constructor with parameters. Distinguishes between
|
|
854
|
+
# constructors (create series) and operations (transform series).
|
|
855
|
+
#
|
|
856
|
+
# @param operation [Symbol] operation or constructor name
|
|
857
|
+
# @param parameter [Object, nil] operation parameters
|
|
858
|
+
#
|
|
859
|
+
# @return [Proc] chainable proc accepting `last` argument
|
|
860
|
+
#
|
|
861
|
+
# @api private
|
|
266
862
|
private def operation_as_chained_proc(operation, parameter = nil)
|
|
267
863
|
if is_a_series_constructor?(operation)
|
|
268
864
|
proc do |last|
|
|
@@ -278,6 +874,31 @@ module Musa
|
|
|
278
874
|
end
|
|
279
875
|
end
|
|
280
876
|
|
|
877
|
+
# Calls series constructor based on previous result type.
|
|
878
|
+
#
|
|
879
|
+
# Handles different `last` types to enable flexible constructor usage:
|
|
880
|
+
#
|
|
881
|
+
# - **UndefinedSerie**: Normal constructor call (pipeline start)
|
|
882
|
+
# - **Array**: Pass array elements as arguments
|
|
883
|
+
# - **Hash**: Pass hash as keyword arguments
|
|
884
|
+
# - **nil**: Call with parameter
|
|
885
|
+
# - **Serie**: Error (cannot reconstruct from serie)
|
|
886
|
+
# - **Proc**: Evaluate proc first, then call constructor
|
|
887
|
+
#
|
|
888
|
+
# This enables patterns like:
|
|
889
|
+
# - `({ S: [1, 2, 3] })` - Direct constructor
|
|
890
|
+
# - `operation, :S` - Constructor from operation result
|
|
891
|
+
#
|
|
892
|
+
# @param last [Object] previous pipeline result
|
|
893
|
+
# @param constructor [Symbol] constructor name (e.g., :S, :H)
|
|
894
|
+
# @param parameter [Object] constructor parameters
|
|
895
|
+
#
|
|
896
|
+
# @return [Serie] constructed series
|
|
897
|
+
#
|
|
898
|
+
# @raise [RuntimeError] if last is Serie (invalid)
|
|
899
|
+
# @raise [RuntimeError] if parameter type unexpected
|
|
900
|
+
#
|
|
901
|
+
# @api private
|
|
281
902
|
private def call_constructor_according_to_last_and_parameter(last, constructor, parameter)
|
|
282
903
|
case last
|
|
283
904
|
when Proc
|
|
@@ -318,6 +939,33 @@ module Musa
|
|
|
318
939
|
end
|
|
319
940
|
end
|
|
320
941
|
|
|
942
|
+
# Calls series operation with appropriate parameter handling.
|
|
943
|
+
#
|
|
944
|
+
# Handles different parameter types for operation calls:
|
|
945
|
+
#
|
|
946
|
+
# - **nil**: No parameter, simple call
|
|
947
|
+
# - **Symbol**: Chained call (e.g., `operation.parameter`)
|
|
948
|
+
# - **Proc**: Block parameter (e.g., `operation { |x| x }`)
|
|
949
|
+
# - **Array of 2 Procs**: Composed block (proc1 >> proc2)
|
|
950
|
+
# - **Other**: Direct parameter
|
|
951
|
+
#
|
|
952
|
+
# @param target [Object] object to call operation on
|
|
953
|
+
# @param operation [Symbol] operation name
|
|
954
|
+
# @param parameter [Object, nil] operation parameter
|
|
955
|
+
#
|
|
956
|
+
# @return [Object] operation result
|
|
957
|
+
#
|
|
958
|
+
# @raise [ArgumentError] if Array parameter invalid
|
|
959
|
+
#
|
|
960
|
+
# @example No parameter
|
|
961
|
+
# call_operation_according_to_parameter(serie, :reverse, nil)
|
|
962
|
+
# # => serie.reverse
|
|
963
|
+
#
|
|
964
|
+
# @example With block
|
|
965
|
+
# call_operation_according_to_parameter(serie, :map, ->(x) { x * 2 })
|
|
966
|
+
# # => serie.map { |x| x * 2 }
|
|
967
|
+
#
|
|
968
|
+
# @api private
|
|
321
969
|
private def call_operation_according_to_parameter(target, operation, parameter)
|
|
322
970
|
case parameter
|
|
323
971
|
when nil
|
|
@@ -337,14 +985,59 @@ module Musa
|
|
|
337
985
|
end
|
|
338
986
|
end
|
|
339
987
|
|
|
988
|
+
# Checks if symbol is a series constructor.
|
|
989
|
+
#
|
|
990
|
+
# @param operation [Symbol] operation name
|
|
991
|
+
#
|
|
992
|
+
# @return [Boolean] true if constructor
|
|
993
|
+
#
|
|
994
|
+
# @api private
|
|
340
995
|
private def is_a_series_constructor?(operation)
|
|
341
996
|
Musa::Series::Constructors.instance_methods.include?(operation)
|
|
342
997
|
end
|
|
343
998
|
|
|
999
|
+
# Checks if symbol is a series operation.
|
|
1000
|
+
#
|
|
1001
|
+
# @param operation [Symbol] operation name
|
|
1002
|
+
#
|
|
1003
|
+
# @return [Boolean] true if operation
|
|
1004
|
+
#
|
|
1005
|
+
# @api private
|
|
344
1006
|
private def is_a_series_operation?(operation)
|
|
345
1007
|
Musa::Series::Operations.instance_methods.include?(operation)
|
|
346
1008
|
end
|
|
347
1009
|
|
|
1010
|
+
# Enables DSL method syntax via method_missing.
|
|
1011
|
+
#
|
|
1012
|
+
# Implements the DSL's natural syntax by intercepting undefined methods:
|
|
1013
|
+
#
|
|
1014
|
+
# - **Series operations/constructors**: Return symbol for parsing
|
|
1015
|
+
# - **With arguments/block**: Create pipeline with those elements
|
|
1016
|
+
# - **Without arguments**: Return symbol for routing
|
|
1017
|
+
#
|
|
1018
|
+
# This allows expressions like:
|
|
1019
|
+
#
|
|
1020
|
+
# ```ruby
|
|
1021
|
+
# composer = Composer.new(input: S(1, 2, 3)) do
|
|
1022
|
+
# # `reverse` → returns :reverse (operation symbol)
|
|
1023
|
+
# # `my_step reverse, { skip: 1 }` → creates pipeline named :my_step
|
|
1024
|
+
# # `route input, to: step1` → uses :step1 symbol for routing
|
|
1025
|
+
#
|
|
1026
|
+
# my_step reverse, { skip: 1 }
|
|
1027
|
+
# route input, to: my_step
|
|
1028
|
+
# route my_step, to: output
|
|
1029
|
+
# end
|
|
1030
|
+
#
|
|
1031
|
+
# composer.output.i.to_a # => [2, 1]
|
|
1032
|
+
# ```
|
|
1033
|
+
#
|
|
1034
|
+
# @param symbol [Symbol] method name called
|
|
1035
|
+
# @param args [Array] method arguments
|
|
1036
|
+
# @param block [Proc, nil] block passed to method
|
|
1037
|
+
#
|
|
1038
|
+
# @return [Symbol, void] symbol for parsing or creates pipeline
|
|
1039
|
+
#
|
|
1040
|
+
# @api private
|
|
348
1041
|
private def method_missing(symbol, *args, &block)
|
|
349
1042
|
if is_a_series_constructor?(symbol) || is_a_series_operation?(symbol)
|
|
350
1043
|
symbol
|
|
@@ -356,6 +1049,14 @@ module Musa
|
|
|
356
1049
|
end
|
|
357
1050
|
end
|
|
358
1051
|
|
|
1052
|
+
# Declares which methods respond_to for method_missing.
|
|
1053
|
+
#
|
|
1054
|
+
# @param method_name [Symbol] method to check
|
|
1055
|
+
# @param include_private [Boolean] include private methods
|
|
1056
|
+
#
|
|
1057
|
+
# @return [Boolean] true if responds to method
|
|
1058
|
+
#
|
|
1059
|
+
# @api private
|
|
359
1060
|
private def respond_to_missing?(method_name, include_private = false)
|
|
360
1061
|
Musa::Series::Operations.instance_methods.include?(method_name) ||
|
|
361
1062
|
Musa::Series::Constructors.instance_methods.include?(method_name) ||
|