Class: HeadMusic::Content::Composition::HashDeserializer

Inherits:
Object
  • Object
show all
Defined in:
lib/head_music/content/composition/hash_deserializer.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. Raw values are validated at the boundary by SchemaValues so corrupted input raises ArgumentError with path context instead of silently deserializing wrong.

Instance Attribute Summary collapse

Instance Method Summary collapse

Constructor Details

#initialize(hash) ⇒ HashDeserializer

Returns a new instance of HashDeserializer.

Raises:

  • (ArgumentError)


10
11
12
13
14
15
# File 'lib/head_music/content/composition/hash_deserializer.rb', line 10

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

#hashObject (readonly, private)

Returns the value of attribute hash.



28
29
30
# File 'lib/head_music/content/composition/hash_deserializer.rb', line 28

def hash
  @hash
end

Instance Method Details

#add_comments(composition) ⇒ Object (private)



98
99
100
101
102
103
104
# File 'lib/head_music/content/composition/hash_deserializer.rb', line 98

def add_comments(composition)
  Array(hash["comments"]).each_with_index do |comment_hash, index|
    raw_position = comment_hash["position"]
    position = values.position(raw_position, "comments[#{index}]") if raw_position
    composition.add_comment(comment_hash["text"], position)
  end
end

#apply_bar_changes(composition) ⇒ Object (private)



60
61
62
63
64
65
66
67
68
69
# File 'lib/head_music/content/composition/hash_deserializer.rb', line 60

def apply_bar_changes(composition)
  bar_hashes.each_with_index do |bar_hash, index|
    number = values.bar_number(bar_hash, index)
    path = "bars[#{index}]"
    key_signature = values.key_signature(bar_hash["key_signature"], path)
    meter = values.meter(bar_hash["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)



85
86
87
88
89
90
91
92
93
94
95
96
# File 'lib/head_music/content/composition/hash_deserializer.rb', line 85

def apply_repeat_flags(composition)
  bar_hashes.each_with_index do |bar_hash, index|
    next unless repeat_state?(bar_hash)

    bar = composition.bars(values.bar_number(bar_hash, index)).last
    bar.starts_repeat = true if bar_hash["starts_repeat"]
    ends_repeat = bar_hash["ends_repeat_after_num_plays"]
    bar.ends_repeat_after_num_plays = ends_repeat if ends_repeat
    plays_on_passes = bar_hash["plays_on_passes"]
    bar.plays_on_passes = plays_on_passes if plays_on_passes
  end
end

#bar_hashesObject (private)



56
57
58
# File 'lib/head_music/content/composition/hash_deserializer.rb', line 56

def bar_hashes
  @bar_hashes ||= Array(hash["bars"])
end

#build_base_compositionObject (private)



46
47
48
49
50
51
52
53
54
# File 'lib/head_music/content/composition/hash_deserializer.rb', line 46

def build_base_composition
  HeadMusic::Content::Composition.new(
    name: hash["name"],
    key_signature: values.key_signature(hash["key_signature"], "key_signature"),
    meter: values.meter(hash["meter"], "meter"),
    composer: hash["composer"],
    origin: hash["origin"]
  )
end

#build_voices(composition) ⇒ Object (private)



71
72
73
74
75
76
77
78
79
80
81
82
83
# File 'lib/head_music/content/composition/hash_deserializer.rb', line 71

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 = values.position(placement_hash["position"], path)
      rhythmic_value = values.rhythmic_value(placement_hash["rhythmic_value"], path)
      sounds = values.placement_sounds(placement_hash, path)
      placement = voice.place(position, rhythmic_value, sounds)
      placement.beam_break_before = placement_hash["beam_break_before"] if placement_hash.key?("beam_break_before")
    end
  end
end

#compositionObject



17
18
19
20
21
22
23
24
# File 'lib/head_music/content/composition/hash_deserializer.rb', line 17

def composition
  @composition ||= build_base_composition.tap do |composition|
    apply_bar_changes(composition)
    build_voices(composition)
    apply_repeat_flags(composition)
    add_comments(composition)
  end
end

#repeat_state?(bar_hash) ⇒ Boolean (private)

Returns:

  • (Boolean)


106
107
108
# File 'lib/head_music/content/composition/hash_deserializer.rb', line 106

def repeat_state?(bar_hash)
  bar_hash["starts_repeat"] || bar_hash["ends_repeat_after_num_plays"] || bar_hash["plays_on_passes"]
end

#validate_schema_versionObject (private)

Raises:

  • (ArgumentError)


34
35
36
37
38
39
40
41
42
43
44
# File 'lib/head_music/content/composition/hash_deserializer.rb', line 34

def validate_schema_version
  version = hash["schema_version"]
  return if version.is_a?(Integer) && version == SCHEMA_VERSION

  message = "unsupported schema_version: #{version.inspect} (supported: #{SCHEMA_VERSION})"
  if version == 2
    message += "; migrate v2 \"pitches\" arrays to v3 \"sounds\" arrays " \
      "(pitch strings unchanged; unpitched sounds are {\"unpitched\" => name_key} objects)"
  end
  raise ArgumentError, message
end

#valuesObject (private)



30
31
32
# File 'lib/head_music/content/composition/hash_deserializer.rb', line 30

def values
  @values ||= SchemaValues.new
end