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 v3 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 value that fails to parse would otherwise silently deserialize as a rest.
-
#parsed_placement_sounds(placement_hash, path) ⇒ Object
private
“sounds” is an array of sound data, empty for 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
-
#parsed_sound(value, path) ⇒ Object
private
-
#parsed_unpitched_sound(value, path) ⇒ Object
private
A nil name_key is the generic unpitched sound.
-
#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)
234 235 236 237 238 239 |
# File 'lib/head_music/content/composition.rb', line 234 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)
199 200 201 202 203 204 205 206 207 208 |
# File 'lib/head_music/content/composition.rb', line 199 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)
223 224 225 226 227 228 229 230 231 232 |
# File 'lib/head_music/content/composition.rb', line 223 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)
195 196 197 |
# File 'lib/head_music/content/composition.rb', line 195 def @bar_hashes ||= Array(hash["bars"]) end |
#build_base_composition ⇒ Object (private)
185 186 187 188 189 190 191 192 193 |
# File 'lib/head_music/content/composition.rb', line 185 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)
210 211 212 213 214 215 216 217 218 219 220 221 |
# File 'lib/head_music/content/composition.rb', line 210 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) sounds = parsed_placement_sounds(placement_hash, path) voice.place(position, rhythmic_value, sounds) 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.
267 268 269 270 271 272 273 274 275 276 277 278 279 |
# File 'lib/head_music/content/composition.rb', line 267 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)
281 282 283 284 285 286 287 288 289 290 291 292 293 |
# File 'lib/head_music/content/composition.rb', line 281 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 value that fails to parse would otherwise silently deserialize as a rest.
315 316 317 318 319 320 |
# File 'lib/head_music/content/composition.rb', line 315 def parsed_pitch(value, path) pitch = HeadMusic::Rudiment::Pitch.get(value) raise ArgumentError, "#{path}: unknown pitch #{value.inspect}" unless pitch pitch end |
#parsed_placement_sounds(placement_hash, path) ⇒ Object (private)
“sounds” is an array of sound data, empty for a rest. A pitched sound is a pitch string; an unpitched sound is a one-key => name_key hash. A nil element is never a rest, so it fails like any other unknown sound.
326 327 328 329 330 331 332 333 334 335 |
# File 'lib/head_music/content/composition.rb', line 326 def parsed_placement_sounds(placement_hash, path) values = placement_hash["sounds"] unless values.is_a?(Array) raise ArgumentError, "#{path}: sounds must be an Array, got #{values.inspect}" end values.each_with_index.map do |value, index| parsed_sound(value, "#{path}.sounds[#{index}]") end 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.
256 257 258 259 260 261 262 263 |
# File 'lib/head_music/content/composition.rb', line 256 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)
295 296 297 298 299 300 301 |
# File 'lib/head_music/content/composition.rb', line 295 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 |
#parsed_sound(value, path) ⇒ Object (private)
337 338 339 340 341 342 |
# File 'lib/head_music/content/composition.rb', line 337 def parsed_sound(value, path) return parsed_pitch(value, path) if value.is_a?(String) return parsed_unpitched_sound(value, path) if value.is_a?(Hash) raise ArgumentError, "#{path}: unknown sound #{value.inspect}" end |
#parsed_unpitched_sound(value, path) ⇒ Object (private)
A nil name_key is the generic unpitched sound. A pitched instrument is a valid hit surface (a knock on a violin body is unpitched), so any catalog name or alias resolves.
347 348 349 350 351 352 353 354 355 356 357 358 |
# File 'lib/head_music/content/composition.rb', line 347 def parsed_unpitched_sound(value, path) unless value.keys == ["unpitched"] raise ArgumentError, "#{path}: unknown sound #{value.inspect}" end name = value["unpitched"] valid_name = name.nil? || (name.is_a?(String) && !name.empty?) sound = HeadMusic::Rudiment::UnpitchedSound.get(name) if valid_name raise ArgumentError, "#{path}: unknown instrument #{name.inspect}" unless sound sound end |
#repeat_state?(bar_hash) ⇒ Boolean (private)
241 242 243 |
# File 'lib/head_music/content/composition.rb', line 241 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.
306 307 308 309 310 311 |
# File 'lib/head_music/content/composition.rb', line 306 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 179 180 181 182 183 |
# 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 = "unsupported schema_version: #{version.inspect} (supported: #{SCHEMA_VERSION})" if version == 2 += "; migrate v2 \"pitches\" arrays to v3 \"sounds\" arrays " \ "(pitch strings unchanged; unpitched sounds are {\"unpitched\" => name_key} objects)" end raise ArgumentError, end |
#validated_bar_number(bar_hash, index) ⇒ Object (private)
245 246 247 248 249 250 251 |
# File 'lib/head_music/content/composition.rb', line 245 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 |