musa-dsl 0.30.2 → 0.41.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (158) hide show
  1. checksums.yaml +4 -4
  2. data/.gitignore +5 -1
  3. data/.version +6 -0
  4. data/.yardopts +7 -0
  5. data/Gemfile +0 -1
  6. data/README.md +227 -6
  7. data/docs/README.md +83 -0
  8. data/docs/api-reference.md +86 -0
  9. data/docs/getting-started/quick-start.md +93 -0
  10. data/docs/getting-started/tutorial.md +58 -0
  11. data/docs/subsystems/core-extensions.md +316 -0
  12. data/docs/subsystems/datasets.md +465 -0
  13. data/docs/subsystems/generative.md +290 -0
  14. data/docs/subsystems/matrix.md +63 -0
  15. data/docs/subsystems/midi.md +123 -0
  16. data/docs/subsystems/music.md +544 -0
  17. data/docs/subsystems/musicxml-builder.md +264 -0
  18. data/docs/subsystems/neumas.md +71 -0
  19. data/docs/subsystems/repl.md +135 -0
  20. data/docs/subsystems/sequencer.md +98 -0
  21. data/docs/subsystems/series.md +302 -0
  22. data/docs/subsystems/transcription.md +152 -0
  23. data/docs/subsystems/transport.md +177 -0
  24. data/lib/musa-dsl/core-ext/array-explode-ranges.rb +68 -0
  25. data/lib/musa-dsl/core-ext/arrayfy.rb +110 -0
  26. data/lib/musa-dsl/core-ext/attribute-builder.rb +91 -30
  27. data/lib/musa-dsl/core-ext/deep-copy.rb +125 -2
  28. data/lib/musa-dsl/core-ext/dynamic-proxy.rb +78 -0
  29. data/lib/musa-dsl/core-ext/extension.rb +53 -0
  30. data/lib/musa-dsl/core-ext/hashify.rb +162 -1
  31. data/lib/musa-dsl/core-ext/inspect-nice.rb +154 -0
  32. data/lib/musa-dsl/core-ext/smart-proc-binder.rb +117 -0
  33. data/lib/musa-dsl/core-ext/with.rb +114 -0
  34. data/lib/musa-dsl/datasets/dataset.rb +109 -0
  35. data/lib/musa-dsl/datasets/delta-d.rb +78 -0
  36. data/lib/musa-dsl/datasets/e.rb +186 -2
  37. data/lib/musa-dsl/datasets/gdv.rb +279 -2
  38. data/lib/musa-dsl/datasets/gdvd.rb +201 -0
  39. data/lib/musa-dsl/datasets/helper.rb +75 -0
  40. data/lib/musa-dsl/datasets/p.rb +177 -2
  41. data/lib/musa-dsl/datasets/packed-v.rb +91 -0
  42. data/lib/musa-dsl/datasets/pdv.rb +136 -1
  43. data/lib/musa-dsl/datasets/ps.rb +134 -4
  44. data/lib/musa-dsl/datasets/score/queriable.rb +143 -1
  45. data/lib/musa-dsl/datasets/score/render.rb +105 -1
  46. data/lib/musa-dsl/datasets/score/to-mxml/process-pdv.rb +138 -1
  47. data/lib/musa-dsl/datasets/score/to-mxml/process-ps.rb +111 -0
  48. data/lib/musa-dsl/datasets/score/to-mxml/process-time.rb +200 -1
  49. data/lib/musa-dsl/datasets/score/to-mxml/to-mxml.rb +145 -1
  50. data/lib/musa-dsl/datasets/score.rb +279 -0
  51. data/lib/musa-dsl/datasets/v.rb +88 -0
  52. data/lib/musa-dsl/generative/darwin.rb +215 -1
  53. data/lib/musa-dsl/generative/generative-grammar.rb +387 -0
  54. data/lib/musa-dsl/generative/markov.rb +135 -3
  55. data/lib/musa-dsl/generative/rules.rb +312 -4
  56. data/lib/musa-dsl/generative/variatio.rb +286 -2
  57. data/lib/musa-dsl/logger/logger.rb +267 -2
  58. data/lib/musa-dsl/matrix/matrix.rb +256 -10
  59. data/lib/musa-dsl/midi/midi-recorder.rb +113 -2
  60. data/lib/musa-dsl/midi/midi-voices.rb +275 -4
  61. data/lib/musa-dsl/music/chord-definition.rb +233 -1
  62. data/lib/musa-dsl/music/chord-definitions.rb +33 -6
  63. data/lib/musa-dsl/music/chords.rb +353 -2
  64. data/lib/musa-dsl/music/equally-tempered-12-tone-scale-system.rb +70 -206
  65. data/lib/musa-dsl/music/scale_kinds/bebop/bebop_dominant_scale_kind.rb +110 -0
  66. data/lib/musa-dsl/music/scale_kinds/bebop/bebop_major_scale_kind.rb +110 -0
  67. data/lib/musa-dsl/music/scale_kinds/bebop/bebop_minor_scale_kind.rb +110 -0
  68. data/lib/musa-dsl/music/scale_kinds/blues/blues_major_scale_kind.rb +100 -0
  69. data/lib/musa-dsl/music/scale_kinds/blues/blues_scale_kind.rb +99 -0
  70. data/lib/musa-dsl/music/scale_kinds/chromatic_scale_kind.rb +79 -0
  71. data/lib/musa-dsl/music/scale_kinds/ethnic/double_harmonic_scale_kind.rb +102 -0
  72. data/lib/musa-dsl/music/scale_kinds/ethnic/hungarian_minor_scale_kind.rb +102 -0
  73. data/lib/musa-dsl/music/scale_kinds/ethnic/neapolitan_major_scale_kind.rb +102 -0
  74. data/lib/musa-dsl/music/scale_kinds/ethnic/neapolitan_minor_scale_kind.rb +101 -0
  75. data/lib/musa-dsl/music/scale_kinds/ethnic/phrygian_dominant_scale_kind.rb +103 -0
  76. data/lib/musa-dsl/music/scale_kinds/harmonic_major/harmonic_major_scale_kind.rb +104 -0
  77. data/lib/musa-dsl/music/scale_kinds/major_scale_kind.rb +110 -0
  78. data/lib/musa-dsl/music/scale_kinds/melodic_minor/altered_scale_kind.rb +106 -0
  79. data/lib/musa-dsl/music/scale_kinds/melodic_minor/dorian_b2_scale_kind.rb +104 -0
  80. data/lib/musa-dsl/music/scale_kinds/melodic_minor/locrian_sharp2_scale_kind.rb +103 -0
  81. data/lib/musa-dsl/music/scale_kinds/melodic_minor/lydian_augmented_scale_kind.rb +103 -0
  82. data/lib/musa-dsl/music/scale_kinds/melodic_minor/lydian_dominant_scale_kind.rb +106 -0
  83. data/lib/musa-dsl/music/scale_kinds/melodic_minor/melodic_minor_scale_kind.rb +104 -0
  84. data/lib/musa-dsl/music/scale_kinds/melodic_minor/mixolydian_b6_scale_kind.rb +103 -0
  85. data/lib/musa-dsl/music/scale_kinds/minor_harmonic_scale_kind.rb +125 -0
  86. data/lib/musa-dsl/music/scale_kinds/minor_natural_scale_kind.rb +123 -0
  87. data/lib/musa-dsl/music/scale_kinds/modes/dorian_scale_kind.rb +111 -0
  88. data/lib/musa-dsl/music/scale_kinds/modes/locrian_scale_kind.rb +114 -0
  89. data/lib/musa-dsl/music/scale_kinds/modes/lydian_scale_kind.rb +111 -0
  90. data/lib/musa-dsl/music/scale_kinds/modes/mixolydian_scale_kind.rb +111 -0
  91. data/lib/musa-dsl/music/scale_kinds/modes/phrygian_scale_kind.rb +111 -0
  92. data/lib/musa-dsl/music/scale_kinds/pentatonic/pentatonic_major_scale_kind.rb +93 -0
  93. data/lib/musa-dsl/music/scale_kinds/pentatonic/pentatonic_minor_scale_kind.rb +99 -0
  94. data/lib/musa-dsl/music/scale_kinds/symmetric/diminished_hw_scale_kind.rb +110 -0
  95. data/lib/musa-dsl/music/scale_kinds/symmetric/diminished_wh_scale_kind.rb +110 -0
  96. data/lib/musa-dsl/music/scale_kinds/symmetric/whole_tone_scale_kind.rb +99 -0
  97. data/lib/musa-dsl/music/scale_systems/equally_tempered_12_tone_scale_system.rb +80 -0
  98. data/lib/musa-dsl/music/scale_systems/twelve_semitones_scale_system.rb +60 -0
  99. data/lib/musa-dsl/music/scales.rb +1384 -40
  100. data/lib/musa-dsl/musicxml/builder/attributes.rb +483 -3
  101. data/lib/musa-dsl/musicxml/builder/backup-forward.rb +166 -1
  102. data/lib/musa-dsl/musicxml/builder/direction.rb +243 -0
  103. data/lib/musa-dsl/musicxml/builder/helper.rb +240 -0
  104. data/lib/musa-dsl/musicxml/builder/measure.rb +284 -0
  105. data/lib/musa-dsl/musicxml/builder/note-complexities.rb +324 -8
  106. data/lib/musa-dsl/musicxml/builder/note.rb +285 -0
  107. data/lib/musa-dsl/musicxml/builder/part-group.rb +108 -1
  108. data/lib/musa-dsl/musicxml/builder/part.rb +139 -0
  109. data/lib/musa-dsl/musicxml/builder/pitched-note.rb +124 -0
  110. data/lib/musa-dsl/musicxml/builder/rest.rb +93 -0
  111. data/lib/musa-dsl/musicxml/builder/score-partwise.rb +276 -0
  112. data/lib/musa-dsl/musicxml/builder/typed-text.rb +62 -1
  113. data/lib/musa-dsl/musicxml/builder/unpitched-note.rb +83 -0
  114. data/lib/musa-dsl/neumalang/neumalang.rb +675 -0
  115. data/lib/musa-dsl/neumas/array-to-neumas.rb +149 -0
  116. data/lib/musa-dsl/neumas/neuma-decoder.rb +253 -0
  117. data/lib/musa-dsl/neumas/neuma-gdv-decoder.rb +142 -2
  118. data/lib/musa-dsl/neumas/neuma-gdvd-decoder.rb +82 -0
  119. data/lib/musa-dsl/neumas/neumas.rb +67 -0
  120. data/lib/musa-dsl/neumas/string-to-neumas.rb +233 -1
  121. data/lib/musa-dsl/repl/repl.rb +550 -0
  122. data/lib/musa-dsl/sequencer/base-sequencer-implementation-every.rb +118 -2
  123. data/lib/musa-dsl/sequencer/base-sequencer-implementation-move.rb +149 -2
  124. data/lib/musa-dsl/sequencer/base-sequencer-implementation-play-helper.rb +296 -0
  125. data/lib/musa-dsl/sequencer/base-sequencer-implementation-play-timed.rb +88 -2
  126. data/lib/musa-dsl/sequencer/base-sequencer-implementation-play.rb +161 -0
  127. data/lib/musa-dsl/sequencer/base-sequencer-implementation.rb +263 -0
  128. data/lib/musa-dsl/sequencer/base-sequencer-tick-based.rb +173 -1
  129. data/lib/musa-dsl/sequencer/base-sequencer-tickless-based.rb +177 -0
  130. data/lib/musa-dsl/sequencer/base-sequencer.rb +710 -10
  131. data/lib/musa-dsl/sequencer/sequencer-dsl.rb +210 -0
  132. data/lib/musa-dsl/sequencer/timeslots.rb +79 -0
  133. data/lib/musa-dsl/series/array-to-serie.rb +37 -1
  134. data/lib/musa-dsl/series/base-series.rb +843 -5
  135. data/lib/musa-dsl/series/buffer-serie.rb +54 -0
  136. data/lib/musa-dsl/series/hash-or-array-serie-splitter.rb +64 -0
  137. data/lib/musa-dsl/series/main-serie-constructors.rb +398 -2
  138. data/lib/musa-dsl/series/main-serie-operations.rb +538 -16
  139. data/lib/musa-dsl/series/proxy-serie.rb +67 -0
  140. data/lib/musa-dsl/series/quantizer-serie.rb +57 -7
  141. data/lib/musa-dsl/series/queue-serie.rb +78 -0
  142. data/lib/musa-dsl/series/series-composer.rb +701 -0
  143. data/lib/musa-dsl/series/timed-serie.rb +473 -28
  144. data/lib/musa-dsl/transcription/from-gdv-to-midi.rb +404 -1
  145. data/lib/musa-dsl/transcription/from-gdv-to-musicxml.rb +118 -0
  146. data/lib/musa-dsl/transcription/from-gdv.rb +84 -1
  147. data/lib/musa-dsl/transcription/transcription.rb +265 -0
  148. data/lib/musa-dsl/transport/clock.rb +125 -0
  149. data/lib/musa-dsl/transport/dummy-clock.rb +89 -2
  150. data/lib/musa-dsl/transport/external-tick-clock.rb +91 -0
  151. data/lib/musa-dsl/transport/input-midi-clock.rb +133 -1
  152. data/lib/musa-dsl/transport/timer-clock.rb +183 -1
  153. data/lib/musa-dsl/transport/timer.rb +83 -0
  154. data/lib/musa-dsl/transport/transport.rb +318 -0
  155. data/lib/musa-dsl/version.rb +2 -1
  156. data/lib/musa-dsl.rb +132 -25
  157. data/musa-dsl.gemspec +25 -18
  158. metadata +158 -16
@@ -1,8 +1,98 @@
1
+ require_relative 'extension'
1
2
  require_relative 'deep-copy'
2
3
 
3
4
  module Musa
4
5
  module Extension
6
+ # Refinement that converts objects, arrays, and hashes into hashes with specified keys.
7
+ #
8
+ # This refinement is crucial for normalizing parameters in the DSL, especially when
9
+ # dealing with musical events that can be specified in multiple formats (positional,
10
+ # hash-based, or mixed).
11
+ #
12
+ # ## Core Behavior
13
+ #
14
+ # - **Object**: Creates hash mapping all keys to the same value
15
+ # - **Array**: Maps keys to array elements in order (consuming array)
16
+ # - **Hash**: Filters and reorders according to specified keys
17
+ # - **Preserves singleton class modules** on hash results
18
+ #
19
+ # ## Use Cases
20
+ #
21
+ # - Converting positional parameters to named parameters
22
+ # - Normalizing mixed parameter formats in musical events
23
+ # - Extracting specific keys from larger hashes
24
+ # - Providing defaults for missing event attributes
25
+ #
26
+ # @example Basic object hashification
27
+ # using Musa::Extension::Hashify
28
+ #
29
+ # 100.hashify(keys: [:velocity, :duration])
30
+ # # => { velocity: 100, duration: 100 }
31
+ #
32
+ # @example Array to hash
33
+ # using Musa::Extension::Hashify
34
+ #
35
+ # [60, 100, 0.5].hashify(keys: [:pitch, :velocity, :duration])
36
+ # # => { pitch: 60, velocity: 100, duration: 0.5 }
37
+ #
38
+ # @example Hash filtering and reordering
39
+ # using Musa::Extension::Hashify
40
+ #
41
+ # { pitch: 60, velocity: 100, channel: 0, duration: 1 }
42
+ # .hashify(keys: [:pitch, :velocity])
43
+ # # => { pitch: 60, velocity: 100 }
44
+ #
45
+ # @example With defaults
46
+ # using Musa::Extension::Hashify
47
+ #
48
+ # [60].hashify(keys: [:pitch, :velocity, :duration], default: nil)
49
+ # # => { pitch: 60, velocity: nil, duration: nil }
50
+ #
51
+ # @example Musical event normalization
52
+ # using Musa::Extension::Hashify
53
+ #
54
+ # # User provides just a pitch
55
+ # 60.hashify(keys: [:pitch, :velocity], default: 64)
56
+ # # => { pitch: 60, velocity: 64 }
57
+ #
58
+ # # User provides array [pitch, velocity, duration]
59
+ # [62, 90, 0.5].hashify(keys: [:pitch, :velocity, :duration])
60
+ # # => { pitch: 62, velocity: 90, duration: 0.5 }
61
+ #
62
+ # @see Musa::Datasets Hash-based event structures
63
+ # @note This refinement must be activated with `using Musa::Extension::Hashify`
64
+ # @note Singleton class modules (dataset extensions) are preserved on hash results
65
+ #
66
+ # ## Methods Added
67
+ #
68
+ # ### Object
69
+ # - {Object#hashify} - Creates a hash mapping all specified keys to this object's value
70
+ #
71
+ # ### Array
72
+ # - {Array#hashify} - Maps array elements to hash keys in order, consuming the array
73
+ #
74
+ # ### Hash
75
+ # - {Hash#hashify} - Filters and reorders hash to include only specified keys, preserving modules
5
76
  module Hashify
77
+ # @!method hashify(keys:, default: nil)
78
+ # Creates a hash mapping all specified keys to this object's value.
79
+ #
80
+ # Useful for broadcasting a single value across multiple attributes.
81
+ # Nil objects can be replaced with a default value.
82
+ #
83
+ # @note This method is added to Object via refinement. Requires `using Musa::Extension::Hashify`.
84
+ #
85
+ # @param keys [Array<Symbol>] keys for the resulting hash.
86
+ # @param default [Object, nil] value to use if self is nil.
87
+ #
88
+ # @return [Hash] hash with all keys mapped to self (or default if nil).
89
+ #
90
+ # @example Single value to multiple keys
91
+ # using Musa::Extension::Hashify
92
+ # 127.hashify(keys: [:velocity, :pressure])
93
+ # # => { velocity: 127, pressure: 127 }
94
+ class ::Object; end
95
+
6
96
  refine Object do
7
97
  def hashify(keys:, default: nil)
8
98
  keys.collect do |key|
@@ -16,6 +106,36 @@ module Musa
16
106
  end
17
107
  end
18
108
 
109
+ # @!method hashify(keys:, default: nil)
110
+ # Maps array elements to hash keys in order, consuming the array.
111
+ #
112
+ # Elements are assigned to keys sequentially. If the array has fewer elements
113
+ # than keys, remaining keys get nil (or default). The array is cloned before
114
+ # consumption, so the original is unchanged.
115
+ #
116
+ # @note This method is added to Array via refinement. Requires `using Musa::Extension::Hashify`.
117
+ #
118
+ # @param keys [Array<Symbol>] keys for the resulting hash (in order).
119
+ # @param default [Object, nil] value for keys without corresponding array elements.
120
+ #
121
+ # @return [Hash] hash with keys mapped to array elements in order.
122
+ #
123
+ # @example Basic array mapping
124
+ # using Musa::Extension::Hashify
125
+ # [60, 100, 0.25].hashify(keys: [:pitch, :velocity, :duration])
126
+ # # => { pitch: 60, velocity: 100, duration: 0.25 }
127
+ #
128
+ # @example Fewer elements than keys
129
+ # using Musa::Extension::Hashify
130
+ # [60, 100].hashify(keys: [:pitch, :velocity, :duration], default: nil)
131
+ # # => { pitch: 60, velocity: 100, duration: nil }
132
+ #
133
+ # @example More elements than keys (extras ignored)
134
+ # using Musa::Extension::Hashify
135
+ # [60, 100, 0.5, :ignored].hashify(keys: [:pitch, :velocity])
136
+ # # => { pitch: 60, velocity: 100 }
137
+ class ::Array; end
138
+
19
139
  refine Array do
20
140
  def hashify(keys:, default: nil)
21
141
  values = clone
@@ -23,10 +143,51 @@ module Musa
23
143
  value = values.shift
24
144
  [ key,
25
145
  value.nil? ? default : value ]
26
- end.to_hash
146
+ end.to_h
27
147
  end
28
148
  end
29
149
 
150
+ # @!method hashify(keys:, default: nil)
151
+ # Filters and reorders hash to include only specified keys, preserving modules.
152
+ #
153
+ # Creates a new hash with only the requested keys, in the order specified.
154
+ # Missing keys get nil (or default). Singleton class modules (like dataset
155
+ # extensions) are copied to the result.
156
+ #
157
+ # @note This method is added to Hash via refinement. Requires `using Musa::Extension::Hashify`.
158
+ #
159
+ # @param keys [Array<Symbol>] keys to include in result (order matters).
160
+ # @param default [Object, nil] value for keys not present in source hash.
161
+ #
162
+ # @return [Hash] new hash with specified keys, preserving singleton modules.
163
+ #
164
+ # @example Filtering keys
165
+ # using Musa::Extension::Hashify
166
+ # { pitch: 60, velocity: 100, channel: 0 }
167
+ # .hashify(keys: [:pitch, :velocity])
168
+ # # => { pitch: 60, velocity: 100 }
169
+ #
170
+ # @example Reordering keys
171
+ # using Musa::Extension::Hashify
172
+ # { velocity: 100, pitch: 60 }
173
+ # .hashify(keys: [:pitch, :velocity])
174
+ # # => { pitch: 60, velocity: 100 }
175
+ #
176
+ # @example Adding missing keys with default
177
+ # using Musa::Extension::Hashify
178
+ # { pitch: 60 }
179
+ # .hashify(keys: [:pitch, :velocity], default: 80)
180
+ # # => { pitch: 60, velocity: 80 }
181
+ #
182
+ # @example Preserving dataset modules
183
+ # using Musa::Extension::Hashify
184
+ # event = { pitch: 60, velocity: 100 }.extend(Musa::Datasets::AbsI)
185
+ # event.hashify(keys: [:pitch, :velocity])
186
+ # # Result also extended with AbsI
187
+ #
188
+ # @note Singleton class modules are preserved via DeepCopy.copy_singleton_class_modules
189
+ class ::Hash; end
190
+
30
191
  refine Hash do
31
192
  def hashify(keys:, default: nil)
32
193
  keys.collect do |key|
@@ -1,6 +1,89 @@
1
+ require_relative 'extension'
2
+
1
3
  module Musa
2
4
  module Extension
5
+ # Refinements that provide more readable inspect/to_s output for Hash and Rational.
6
+ #
7
+ # These refinements improve readability of log output and debugging, especially
8
+ # important when working with musical data that heavily uses Rationals for timing
9
+ # and Hashes for event parameters.
10
+ #
11
+ # ## Changes
12
+ #
13
+ # - **Hash**: Compact syntax with symbol keys shown as `key: value`
14
+ # - **Rational**: Musical-friendly format like `3+1/4r` instead of `(13/4)`
15
+ # - **Configurable**: Rational display can switch between simple and detailed modes
16
+ #
17
+ # ## Use Cases
18
+ #
19
+ # - Improving log readability in musical applications
20
+ # - Debugging DSL expressions with cleaner output
21
+ # - Displaying musical time values (bars, durations) naturally
22
+ #
23
+ # @example Hash formatting
24
+ # using Musa::Extension::InspectNice
25
+ #
26
+ # { pitch: 60, velocity: 100 }.inspect
27
+ # # => "{ pitch: 60, velocity: 100 }"
28
+ # # Instead of: "{:pitch=>60, :velocity=>100}"
29
+ #
30
+ # @example Rational formatting (detailed mode)
31
+ # using Musa::Extension::InspectNice
32
+ #
33
+ # (5/4r).inspect # => "1+1/4r"
34
+ # (3/2r).inspect # => "1+1/2r"
35
+ # (2/1r).inspect # => "2r"
36
+ # (-3/4r).inspect # => "-3/4r"
37
+ #
38
+ # @example Rational formatting (simple mode)
39
+ # using Musa::Extension::InspectNice
40
+ #
41
+ # Rational.to_s_as_inspect = false
42
+ # (5/4r).to_s # => "5/4"
43
+ # (2/1r).to_s # => "2"
44
+ #
45
+ # @see Musa::Logger::Logger Uses these refinements for cleaner logs
46
+ # @note These refinements must be activated with `using Musa::Extension::InspectNice`
47
+ #
48
+ # ## Methods Added
49
+ #
50
+ # ### Hash
51
+ # - {Hash#inspect} - Compact, readable inspect output with symbol-key shorthand
52
+ # - {Hash#to_s} - Aliases to_s to inspect for consistency
53
+ #
54
+ # ### Rational (singleton class)
55
+ # - {Rational.to_s_as_inspect} - Controls whether Rational#to_s uses inspect format
56
+ #
57
+ # ### Rational
58
+ # - {Rational#inspect} - Musical-friendly inspect output for Rational numbers
59
+ # - {Rational#to_s} - String representation controlled by Rational.to_s_as_inspect
3
60
  module InspectNice
61
+ # @!method inspect
62
+ # Provides compact, readable inspect output with symbol-key shorthand.
63
+ #
64
+ # Symbol keys are displayed as `key: value` (Ruby 2.0+ syntax) instead of
65
+ # `:key => value`. String/other keys use the fat arrow syntax.
66
+ #
67
+ # @note This method is added to Hash via refinement. Requires `using Musa::Extension::InspectNice`.
68
+ #
69
+ # @return [String] compact hash representation.
70
+ #
71
+ # @example Mixed keys
72
+ # using Musa::Extension::InspectNice
73
+ # { pitch: 60, 'name' => 'C4' }.inspect
74
+ # # => "{ pitch: 60, 'name' => 'C4' }"
75
+ class ::Hash; end
76
+
77
+ # @!method to_s
78
+ # Aliases to_s to inspect for consistency.
79
+ #
80
+ # @note This method is added to Hash via refinement. Requires `using Musa::Extension::InspectNice`.
81
+ #
82
+ # @return [String] compact hash representation.
83
+ #
84
+ # @see Hash#inspect
85
+ class ::Hash; end
86
+
4
87
  refine Hash do
5
88
  def inspect
6
89
  all = collect { |key, value| [', ', key.is_a?(Symbol) ? key.to_s + ': ' : key.inspect + ' => ', value.inspect] }.flatten
@@ -12,10 +95,81 @@ module Musa
12
95
  alias to_s inspect
13
96
  end
14
97
 
98
+ # Adds configuration attribute to Rational singleton class.
99
+ #
100
+ # This allows global control of Rational#to_s behavior.
101
+ #
102
+ # @!attribute [rw] to_s_as_inspect
103
+ # Controls whether Rational#to_s uses inspect format.
104
+ #
105
+ # When true: to_s displays detailed format (e.g., "1+1/4r")
106
+ # When false/nil: to_s displays simple format (e.g., "5/4")
107
+ #
108
+ # @note This attribute is added to Rational's singleton class via refinement. Requires `using Musa::Extension::InspectNice`.
109
+ #
110
+ # @return [Boolean, nil] current mode
111
+ #
112
+ # @example Switching modes
113
+ # using Musa::Extension::InspectNice
114
+ # Rational.to_s_as_inspect = true
115
+ # (5/4r).to_s # => "1+1/4r"
116
+ class ::Rational; end
117
+
15
118
  refine Rational.singleton_class do
16
119
  attr_accessor :to_s_as_inspect
17
120
  end
18
121
 
122
+ # @!method inspect(simple: nil)
123
+ # Provides musical-friendly inspect output for Rational numbers.
124
+ #
125
+ # Two modes:
126
+ # - **Simple**: Just numerator/denominator (e.g., "5/4", "2")
127
+ # - **Detailed**: Mixed number with 'r' suffix (e.g., "1+1/4r", "2r")
128
+ #
129
+ # The detailed format is particularly useful for musical time values,
130
+ # making expressions like "3+1/2r" (3.5 bars) immediately readable.
131
+ #
132
+ # @note This method is added to Rational via refinement. Requires `using Musa::Extension::InspectNice`.
133
+ #
134
+ # @param simple [Boolean, nil] if true, uses simple format; if false/nil, uses detailed.
135
+ #
136
+ # @return [String] formatted rational.
137
+ #
138
+ # @example Detailed format (default for inspect)
139
+ # using Musa::Extension::InspectNice
140
+ # (5/4r).inspect # => "1+1/4r"
141
+ # (7/4r).inspect # => "1+3/4r"
142
+ # (-3/2r).inspect # => "-1-1/2r"
143
+ # (8/4r).inspect # => "2r"
144
+ # (3/4r).inspect # => "3/4r"
145
+ #
146
+ # @example Simple format
147
+ # using Musa::Extension::InspectNice
148
+ # (5/4r).inspect(simple: true) # => "5/4"
149
+ # (8/4r).inspect(simple: true) # => "2"
150
+ class ::Rational; end
151
+
152
+ # @!method to_s
153
+ # Provides string representation, format controlled by Rational.to_s_as_inspect.
154
+ #
155
+ # Delegates to #inspect with the appropriate simple flag based on the
156
+ # global Rational.to_s_as_inspect setting.
157
+ #
158
+ # @note This method is added to Rational via refinement. Requires `using Musa::Extension::InspectNice`.
159
+ #
160
+ # @return [String] formatted rational.
161
+ #
162
+ # @example When to_s_as_inspect is true
163
+ # using Musa::Extension::InspectNice
164
+ # Rational.to_s_as_inspect = true
165
+ # (5/4r).to_s # => "1+1/4r"
166
+ #
167
+ # @example When to_s_as_inspect is false/nil
168
+ # using Musa::Extension::InspectNice
169
+ # Rational.to_s_as_inspect = false
170
+ # (5/4r).to_s # => "5/4"
171
+ class ::Rational; end
172
+
19
173
  refine Rational do
20
174
  def inspect(simple: nil)
21
175
  value = self.abs
@@ -1,17 +1,75 @@
1
+ require_relative 'extension'
2
+
1
3
  module Musa
2
4
  module Extension
5
+ # Module providing smart parameter binding for Proc objects.
6
+ #
7
+ # SmartProcBinder analyzes a Proc's parameter signature and intelligently
8
+ # matches provided arguments to expected parameters, handling both positional
9
+ # and keyword arguments with proper rest parameter support.
10
+ #
11
+ # @see Musa::Extension::With Uses SmartProcBinder for parameter management
3
12
  module SmartProcBinder
13
+ # Wrapper for Proc objects that provides intelligent parameter matching and binding.
14
+ #
15
+ # This class introspects a Proc's parameter list and provides methods to:
16
+ # - Determine which parameters the Proc accepts
17
+ # - Filter provided arguments to match the Proc's signature
18
+ # - Call the Proc with properly matched arguments
19
+ # - Optionally rescue and handle exceptions
20
+ #
21
+ # ## Parameter Types Handled
22
+ #
23
+ # - **:req, :opt**: Required and optional positional parameters
24
+ # - **:rest**: Splat parameter (*args)
25
+ # - **:key, :keyreq**: Optional and required keyword parameters
26
+ # - **:keyrest**: Double splat parameter (**kwargs)
27
+ #
28
+ # ## Use Cases
29
+ #
30
+ # - DSL methods that need flexible parameter passing
31
+ # - Builder patterns with variable block signatures
32
+ # - Wrapper methods that forward arguments intelligently
33
+ # - Error handling for DSL block execution
34
+ #
35
+ # @example Basic usage
36
+ # block = proc { |a, b, c:| [a, b, c] }
37
+ # binder = SmartProcBinder.new(block)
38
+ #
39
+ # binder.call(1, 2, 3, 4, c: 5, d: 6)
40
+ # # => [1, 2, 5]
41
+ # # Only passes parameters that match signature
42
+ #
43
+ # @example With rescue handling
44
+ # error_handler = proc { |e| puts "Error: #{e.message}" }
45
+ # binder = SmartProcBinder.new(block, on_rescue: error_handler)
46
+ #
47
+ # binder.call(invalid_args) # Calls error_handler instead of raising
48
+ #
49
+ # @example Checking parameter support
50
+ # binder.key?(:pitch) # => true/false
51
+ # binder.has_key?(:velocity) # => true/false
4
52
  class SmartProcBinder
53
+ # Creates a new SmartProcBinder wrapping the given block.
54
+ #
55
+ # Introspects the block's parameters and categorizes them for later matching.
56
+ #
57
+ # @param block [Proc] the proc/block to wrap.
58
+ # @param on_rescue [Proc, nil] optional error handler called with exception
59
+ # if block execution fails.
5
60
  def initialize(block, on_rescue: nil)
6
61
  @block = block
7
62
  @on_rescue = on_rescue
8
63
 
64
+ # Track keyword parameters by name
9
65
  @key_parameters = {}
10
66
  @has_key_rest = false
11
67
 
68
+ # Track positional parameter count
12
69
  @value_parameters_count = 0
13
70
  @has_value_rest = false
14
71
 
72
+ # Introspect block's parameter signature
15
73
  block.parameters.each do |parameter|
16
74
  @key_parameters[parameter[1]] = nil if parameter[0] == :key || parameter[0] == :keyreq
17
75
  @has_key_rest = true if parameter[0] == :keyrest
@@ -21,18 +79,36 @@ module Musa
21
79
  end
22
80
  end
23
81
 
82
+ # Returns the wrapped Proc.
83
+ #
84
+ # @return [Proc] the original block.
24
85
  def proc
25
86
  @block
26
87
  end
27
88
 
89
+ # Returns the parameter signature of the wrapped Proc.
90
+ #
91
+ # @return [Array<Array>] array of [type, name] pairs describing parameters.
92
+ # @example
93
+ # proc { |a, b, c:| }.parameters # => [[:req, :a], [:req, :b], [:key, :c]]
28
94
  def parameters
29
95
  @block.parameters
30
96
  end
31
97
 
98
+ # Calls the wrapped Proc with smart parameter matching.
99
+ #
100
+ # @param value_parameters [Array] positional arguments.
101
+ # @param key_parameters [Hash] keyword arguments.
102
+ # @param block [Proc, nil] block to pass to wrapped Proc.
103
+ #
104
+ # @return [Object] result of calling the wrapped Proc.
32
105
  def call(*value_parameters, **key_parameters, &block)
33
106
  _call value_parameters, key_parameters, block
34
107
  end
35
108
 
109
+ # Internal call implementation with error handling.
110
+ #
111
+ # @api private
36
112
  def _call(value_parameters, key_parameters = {}, block = nil)
37
113
  if @on_rescue
38
114
  begin
@@ -63,16 +139,43 @@ module Musa
63
139
  end
64
140
  end
65
141
 
142
+ # Checks if the wrapped Proc accepts a specific keyword parameter.
143
+ #
144
+ # Returns true if the Proc has a keyword parameter with the given name,
145
+ # or if it has a **kwargs rest parameter that accepts any keyword.
146
+ #
147
+ # @param key [Symbol] keyword parameter name to check.
148
+ #
149
+ # @return [Boolean] true if key is accepted, false otherwise.
150
+ #
151
+ # @example
152
+ # proc { |a:, b:, **rest| }.key?(:a) # => true
153
+ # proc { |a:, b:, **rest| }.key?(:unknown) # => true (has **rest)
154
+ # proc { |a:, b:| }.key?(:unknown) # => false
66
155
  def key?(key)
67
156
  @has_key_rest || @key_parameters.include?(key)
68
157
  end
69
158
 
70
159
  alias_method :has_key?, :key?
71
160
 
161
+ # Filters arguments to match the Proc's signature.
162
+ #
163
+ # @param value_parameters [Array] positional arguments to filter.
164
+ # @param key_parameters [Hash] keyword arguments to filter.
165
+ #
166
+ # @return [Array<Array, Hash>] tuple of [filtered_positionals, filtered_keywords].
72
167
  def apply(*value_parameters, **key_parameters)
73
168
  _apply(value_parameters, key_parameters)
74
169
  end
75
170
 
171
+ # Internal implementation of argument filtering.
172
+ #
173
+ # Logic:
174
+ # - Positional: takes first N values (or all if *rest present)
175
+ # - Keywords: includes only expected keys (or all if **rest present)
176
+ # - Pads positional with nils if needed
177
+ #
178
+ # @api private
76
179
  def _apply(value_parameters, key_parameters)
77
180
  value_parameters ||= []
78
181
  key_parameters ||= {}
@@ -99,6 +202,20 @@ module Musa
99
202
  return values_result, hash_result
100
203
  end
101
204
 
205
+ # Returns a string representation of the SmartProcBinder for debugging.
206
+ #
207
+ # Shows the wrapped Proc's parameter signature and internal state, making
208
+ # it easy to understand what parameters the binder expects and how it will
209
+ # match arguments.
210
+ #
211
+ # @return [String] formatted string showing parameters and configuration.
212
+ #
213
+ # @example Inspecting binder state
214
+ # block = proc { |a, b, c:, **rest| }
215
+ # binder = SmartProcBinder.new(block)
216
+ # puts binder.inspect
217
+ # # => "SmartProcBinder: parameters = [[:req, :a], [:req, :b], [:key, :c], [:keyrest, :rest]]
218
+ # # key_parameters = {:c=>nil} has_rest = true"
102
219
  def inspect
103
220
  "SmartProcBinder: parameters = #{parameters} key_parameters = #{@key_parameters} has_rest = #{@has_key_rest}"
104
221
  end
@@ -1,28 +1,142 @@
1
+ require_relative 'extension'
1
2
  require_relative 'smart-proc-binder'
2
3
 
3
4
  module Musa
4
5
  module Extension
6
+ # Module providing the `with` method for flexible DSL block execution.
7
+ #
8
+ # The `with` method is a cornerstone of Musa DSL's builder pattern, allowing
9
+ # objects to execute blocks in either the object's context (for DSL-style
10
+ # configuration) or the caller's context (for traditional Ruby blocks).
11
+ #
12
+ # ## Context Switching Logic
13
+ #
14
+ # The method intelligently determines which context to use based on:
15
+ # 1. The `keep_block_context` parameter (explicit control)
16
+ # 2. Block parameters (implicit control via `_` parameter)
17
+ # 3. Whether parameters are passed to the block
18
+ #
19
+ # ## Modes of Operation
20
+ #
21
+ # - **DSL mode** (`instance_eval`): Block executes in object's context
22
+ # - Used when: no parameters, no `keep_block_context`, no `_` parameter
23
+ # - Enables: direct access to object's instance variables and methods
24
+ #
25
+ # - **Caller context mode** (`call` with self as `_`): Block keeps its context
26
+ # - Used when: block has `_` parameter, or `keep_block_context: true`
27
+ # - Enables: access to both contexts (object via `_`, caller's scope naturally)
28
+ #
29
+ # - **Hybrid mode** (`instance_exec`): Block in object context with parameters
30
+ # - Used when: parameters provided but `keep_block_context` not set
31
+ # - Enables: DSL-style access plus explicit parameters
32
+ #
33
+ # ## Use Cases
34
+ #
35
+ # - Builder pattern DSL methods
36
+ # - Configuration blocks that need object context
37
+ # - Flexible API supporting both DSL and traditional Ruby styles
38
+ # - Initializers that configure objects via blocks
39
+ #
40
+ # @example DSL mode (instance_eval)
41
+ # class Builder
42
+ # include Musa::Extension::With
43
+ #
44
+ # def initialize(&block)
45
+ # @items = []
46
+ # with(&block) if block
47
+ # end
48
+ #
49
+ # def add(item)
50
+ # @items << item
51
+ # end
52
+ # end
53
+ #
54
+ # builder = Builder.new do
55
+ # add :foo
56
+ # add :bar
57
+ # end
58
+ # # Block has direct access to #add method
59
+ #
60
+ # @example Caller context with _ parameter
61
+ # external_var = 42
62
+ #
63
+ # Builder.new do |_|
64
+ # _.add :foo
65
+ # puts external_var # Can access caller's variables
66
+ # end
67
+ # # Block keeps caller's context, object accessed via _
68
+ #
69
+ # @example With parameters
70
+ # class Builder
71
+ # def initialize(name, &block)
72
+ # @name = name
73
+ # with(name, &block) if block
74
+ # end
75
+ # end
76
+ #
77
+ # Builder.new('test') do |name|
78
+ # # Has access to object's context AND receives name parameter
79
+ # puts @name # Works
80
+ # puts name # Also works
81
+ # end
82
+ #
83
+ # @example Explicit keep_block_context
84
+ # Builder.new do |obj|
85
+ # obj.add :item
86
+ # # Block explicitly keeps caller's context
87
+ # end
88
+ #
89
+ # @see SmartProcBinder Used internally for parameter management
90
+ # @see Musa::Datasets DSL builder methods use this extensively
5
91
  module With
92
+ # Executes a block with flexible context and parameter handling.
93
+ #
94
+ # @param value_parameters [Array] positional parameters to pass to block.
95
+ # @param keep_block_context [Boolean, nil] explicit control of context switching:
96
+ # - `true`: always keep caller's context
97
+ # - `false`: always use object's context
98
+ # - `nil`: auto-detect based on `_` parameter
99
+ # @param key_parameters [Hash] keyword parameters to pass to block.
100
+ # @param block [Proc] block to execute.
101
+ #
102
+ # @return [Object] result of block execution.
103
+ #
104
+ # @note The `_` parameter is special: when present, it signals "keep caller's context"
105
+ # and receives `self` (the object) as its value.
106
+ # @note Uses SmartProcBinder internally to handle parameter matching.
6
107
  def with(*value_parameters, keep_block_context: nil, **key_parameters, &block)
108
+ # Wrap block in SmartProcBinder for parameter introspection and management
7
109
  smart_block = Musa::Extension::SmartProcBinder::SmartProcBinder.new(block)
8
110
 
111
+ # Check if first parameter is _ (underscore), which signals "keep caller's context"
9
112
  send_self_as_underscore_parameter = smart_block.parameters[0][1] == :_ unless smart_block.parameters.empty?
10
113
 
114
+ # Determine effective context mode:
115
+ # 1. Use explicit keep_block_context if provided
116
+ # 2. Otherwise, use _ parameter presence as signal
117
+ # 3. Default to false (use object's context)
11
118
  effective_keep_block_context = keep_block_context
12
119
  effective_keep_block_context = send_self_as_underscore_parameter if effective_keep_block_context.nil?
13
120
  effective_keep_block_context = false if effective_keep_block_context.nil?
14
121
 
122
+ # Match provided parameters to block's expected parameters
15
123
  effective_value_parameters, effective_key_parameters = smart_block._apply(value_parameters, key_parameters)
16
124
 
125
+ # Execute block in appropriate context
17
126
  if effective_keep_block_context
127
+ # Keep caller's context: call block normally
18
128
  if send_self_as_underscore_parameter
129
+ # Pass self as first parameter (the _ parameter)
19
130
  smart_block.call(self, *effective_value_parameters, **effective_key_parameters)
20
131
  else
132
+ # Just pass the effective parameters
21
133
  smart_block.call(*effective_value_parameters, **effective_key_parameters)
22
134
  end
23
135
  elsif effective_value_parameters.empty? && effective_key_parameters.empty?
136
+ # DSL mode: no parameters, execute in object's context
24
137
  instance_eval &block
25
138
  else
139
+ # Hybrid mode: execute in object's context with parameters
26
140
  instance_exec *effective_value_parameters, **effective_key_parameters, &block
27
141
  end
28
142
  end