Class: HeadMusic::Content::Composition::HashDeserializer
- Inherits:
-
Object
- Object
- HeadMusic::Content::Composition::HashDeserializer
- Defined in:
- lib/head_music/content/composition.rb
Overview
Rebuilds a composition from a schema v1 hash by replaying the public builder API in dependency order: meter and key changes first (position strings roll counts and ticks over via the meter map), then placements, then repeat flags (a pickup-bar flag needs its bar allocated), then comments. Validates values at the boundary so corrupted input raises ArgumentError with path context instead of silently deserializing wrong.
Instance Attribute Summary collapse
-
#hash ⇒ Object
readonly
private
Returns the value of attribute hash.
Instance Method Summary collapse
-
#add_comments(composition) ⇒ Object
private
-
#apply_bar_changes(composition) ⇒ Object
private
-
#apply_repeat_flags(composition) ⇒ Object
private
-
#bar_hashes ⇒ Object
private
-
#build_base_composition ⇒ Object
private
-
#build_voices(composition) ⇒ Object
private
-
#composition ⇒ Object
-
#initialize(hash) ⇒ HashDeserializer
constructor
A new instance of HashDeserializer.
-
#parsed_key_signature(value, path) ⇒ Object
private
KeySignature.get returns a hollow object (nil tonic_spelling) for garbage rather than nil, so presence of the tonic is the real check.
-
#parsed_meter(value, path) ⇒ Object
private
-
#parsed_pitch(value, path) ⇒ Object
private
A nil pitch is a rest; a non-nil string that fails to parse would otherwise silently deserialize as a rest.
-
#parsed_position(value, path) ⇒ Object
private
Position silently coerces garbage strings to “0:1:000”, which would mislocate content with no error, so the shape is validated up front.
-
#parsed_rhythmic_value(value, path) ⇒ Object
private
-
#repeat_state?(bar_hash) ⇒ Boolean
private
-
#valid_rhythmic_value?(rhythmic_value) ⇒ Boolean
private
RhythmicValue.get returns a hollow object (nil unit) for garbage rather than nil, and a tied tail can be hollow while the head parses, so the whole tie chain is checked.
-
#validate_schema_version ⇒ Object
private
-
#validated_bar_number(bar_hash, index) ⇒ Object
private
Constructor Details
#initialize(hash) ⇒ HashDeserializer
Returns a new instance of HashDeserializer.
153 154 155 156 157 158 |
# File 'lib/head_music/content/composition.rb', line 153 def initialize(hash) raise ArgumentError, "expected a Hash, got #{hash.class}" unless hash.is_a?(Hash) @hash = hash.deep_transform_keys(&:to_s) validate_schema_version end |
Instance Attribute Details
#hash ⇒ Object (readonly, private)
Returns the value of attribute hash.
171 172 173 |
# File 'lib/head_music/content/composition.rb', line 171 def hash @hash end |
Instance Method Details
#add_comments(composition) ⇒ Object (private)
229 230 231 232 233 234 |
# File 'lib/head_music/content/composition.rb', line 229 def add_comments(composition) Array(hash["comments"]).each_with_index do |comment_hash, index| position = parsed_position(comment_hash["position"], "comments[#{index}]") if comment_hash["position"] composition.add_comment(comment_hash["text"], position) end end |
#apply_bar_changes(composition) ⇒ Object (private)
194 195 196 197 198 199 200 201 202 203 |
# File 'lib/head_music/content/composition.rb', line 194 def (composition) .each_with_index do |, index| number = (, index) path = "bars[#{index}]" key_signature = parsed_key_signature(["key_signature"], path) meter = parsed_meter(["meter"], path) composition.change_key_signature(number, key_signature) if key_signature composition.change_meter(number, meter) if meter end end |
#apply_repeat_flags(composition) ⇒ Object (private)
218 219 220 221 222 223 224 225 226 227 |
# File 'lib/head_music/content/composition.rb', line 218 def apply_repeat_flags(composition) .each_with_index do |, index| next unless repeat_state?() = composition.((, index)).last .starts_repeat = true if ["starts_repeat"] .ends_repeat_after_num_plays = ["ends_repeat_after_num_plays"] if ["ends_repeat_after_num_plays"] .plays_on_passes = ["plays_on_passes"] if ["plays_on_passes"] end end |
#bar_hashes ⇒ Object (private)
190 191 192 |
# File 'lib/head_music/content/composition.rb', line 190 def @bar_hashes ||= Array(hash["bars"]) end |
#build_base_composition ⇒ Object (private)
180 181 182 183 184 185 186 187 188 |
# File 'lib/head_music/content/composition.rb', line 180 def build_base_composition HeadMusic::Content::Composition.new( name: hash["name"], key_signature: parsed_key_signature(hash["key_signature"], "key_signature"), meter: parsed_meter(hash["meter"], "meter"), composer: hash["composer"], origin: hash["origin"] ) end |
#build_voices(composition) ⇒ Object (private)
205 206 207 208 209 210 211 212 213 214 215 216 |
# File 'lib/head_music/content/composition.rb', line 205 def build_voices(composition) Array(hash["voices"]).each_with_index do |voice_hash, voice_index| voice = composition.add_voice(role: voice_hash["role"]) Array(voice_hash["placements"]).each_with_index do |placement_hash, placement_index| path = "voices[#{voice_index}].placements[#{placement_index}]" position = parsed_position(placement_hash["position"], path) rhythmic_value = parsed_rhythmic_value(placement_hash["rhythmic_value"], path) pitch = parsed_pitch(placement_hash["pitch"], path) voice.place(position, rhythmic_value, pitch) end end end |
#composition ⇒ Object
160 161 162 163 164 165 166 167 |
# File 'lib/head_music/content/composition.rb', line 160 def composition @composition ||= build_base_composition.tap do |composition| (composition) build_voices(composition) apply_repeat_flags(composition) add_comments(composition) end end |
#parsed_key_signature(value, path) ⇒ Object (private)
KeySignature.get returns a hollow object (nil tonic_spelling) for garbage rather than nil, so presence of the tonic is the real check.
262 263 264 265 266 267 268 269 270 271 272 273 274 |
# File 'lib/head_music/content/composition.rb', line 262 def parsed_key_signature(value, path) return nil if value.nil? key_signature = begin HeadMusic::Rudiment::KeySignature.get(value) rescue nil end unless key_signature&.tonic_spelling raise ArgumentError, "#{path}: unknown key signature #{value.inspect}" end key_signature end |
#parsed_meter(value, path) ⇒ Object (private)
276 277 278 279 280 281 282 283 284 285 286 287 288 |
# File 'lib/head_music/content/composition.rb', line 276 def parsed_meter(value, path) return nil if value.nil? meter = begin HeadMusic::Rudiment::Meter.get(value) rescue nil end unless meter&.top_number&.positive? && meter.bottom_number.positive? raise ArgumentError, "#{path}: unknown meter #{value.inspect}" end meter end |
#parsed_pitch(value, path) ⇒ Object (private)
A nil pitch is a rest; a non-nil string that fails to parse would otherwise silently deserialize as a rest.
310 311 312 313 314 315 316 317 |
# File 'lib/head_music/content/composition.rb', line 310 def parsed_pitch(value, path) return nil if value.nil? pitch = HeadMusic::Rudiment::Pitch.get(value) raise ArgumentError, "#{path}: unknown pitch #{value.inspect}" unless pitch pitch end |
#parsed_position(value, path) ⇒ Object (private)
Position silently coerces garbage strings to “0:1:000”, which would mislocate content with no error, so the shape is validated up front. Accepts “bar”, “bar:count”, or “bar:count:tick” with non-negative parts.
251 252 253 254 255 256 257 258 |
# File 'lib/head_music/content/composition.rb', line 251 def parsed_position(value, path) return nil if value.nil? unless value.is_a?(String) && value.match?(/\A\d+(:\d+){0,2}\z/) raise ArgumentError, "#{path}: unknown position #{value.inspect}" end value end |
#parsed_rhythmic_value(value, path) ⇒ Object (private)
290 291 292 293 294 295 296 |
# File 'lib/head_music/content/composition.rb', line 290 def parsed_rhythmic_value(value, path) rhythmic_value = HeadMusic::Rudiment::RhythmicValue.get(value) unless valid_rhythmic_value?(rhythmic_value) raise ArgumentError, "#{path}: unknown rhythmic value #{value.inspect}" end rhythmic_value end |
#repeat_state?(bar_hash) ⇒ Boolean (private)
236 237 238 |
# File 'lib/head_music/content/composition.rb', line 236 def repeat_state?() ["starts_repeat"] || ["ends_repeat_after_num_plays"] || ["plays_on_passes"] end |
#valid_rhythmic_value?(rhythmic_value) ⇒ Boolean (private)
RhythmicValue.get returns a hollow object (nil unit) for garbage rather than nil, and a tied tail can be hollow while the head parses, so the whole tie chain is checked.
301 302 303 304 305 306 |
# File 'lib/head_music/content/composition.rb', line 301 def valid_rhythmic_value?(rhythmic_value) return false unless rhythmic_value.is_a?(HeadMusic::Rudiment::RhythmicValue) return false unless rhythmic_value.unit rhythmic_value.tied_value.nil? || valid_rhythmic_value?(rhythmic_value.tied_value) end |
#validate_schema_version ⇒ Object (private)
173 174 175 176 177 178 |
# File 'lib/head_music/content/composition.rb', line 173 def validate_schema_version version = hash["schema_version"] return if version.is_a?(Integer) && version == SCHEMA_VERSION raise ArgumentError, "unsupported schema_version: #{version.inspect} (supported: #{SCHEMA_VERSION})" end |
#validated_bar_number(bar_hash, index) ⇒ Object (private)
240 241 242 243 244 245 246 |
# File 'lib/head_music/content/composition.rb', line 240 def (, index) number = ["number"] unless number.is_a?(Integer) && number >= 0 raise ArgumentError, "bars[#{index}]: bar number must be an Integer of at least 0, got #{number.inspect}" end number end |