Class: HeadMusic::Content::Composition::SchemaValues

Inherits:
Object
  • Object
show all
Defined in:
lib/head_music/content/composition/schema_values.rb

Overview

Validates and coerces raw schema values into domain objects for the HashDeserializer. Every method takes the raw value and the path it came from, returning a validated object or raising ArgumentError with that path context. Stateless: it holds no reference to the source hash, so the deserializer stays responsible for where values come from and this class for what a value is allowed to be.

Instance Method Summary collapse

Instance Method Details

#bar_number(bar_hash, index) ⇒ Object



93
94
95
96
97
98
99
# File 'lib/head_music/content/composition/schema_values.rb', line 93

def bar_number(bar_hash, index)
  number = bar_hash["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

#key_signature(value, path) ⇒ Object

KeySignature.get returns a hollow object (nil tonic_spelling) for garbage rather than nil, so presence of the tonic is the real check.



23
24
25
26
27
28
29
30
31
32
33
34
35
# File 'lib/head_music/content/composition/schema_values.rb', line 23

def 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

#meter(value, path) ⇒ Object



37
38
39
40
41
42
43
44
45
46
47
48
49
# File 'lib/head_music/content/composition/schema_values.rb', line 37

def 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

#pitch(value, path) ⇒ Object (private)

A value that fails to parse would otherwise silently deserialize as a rest.

Raises:

  • (ArgumentError)


116
117
118
119
120
121
# File 'lib/head_music/content/composition/schema_values.rb', line 116

def pitch(value, path)
  pitch = HeadMusic::Rudiment::Pitch.get(value)
  raise ArgumentError, "#{path}: unknown pitch #{value.inspect}" unless pitch

  pitch
end

#placement_sounds(placement_hash, path) ⇒ Object

“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.



63
64
65
66
67
68
69
70
71
72
# File 'lib/head_music/content/composition/schema_values.rb', line 63

def 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|
    sound(value, "#{path}.sounds[#{index}]")
  end
end

#placement_syllables(placement_hash, path) ⇒ Object

“syllables” is an optional array of sung-text data, one entry per verse. Each entry is a => …, “verse” => …, “hyphen_after” => … hash; verse defaults to 1. Text must be a non-empty string, verse a positive integer, and no two entries may share a verse (a placement holds at most one syllable per verse).



79
80
81
82
83
84
85
86
87
88
89
90
91
# File 'lib/head_music/content/composition/schema_values.rb', line 79

def placement_syllables(placement_hash, path)
  values = placement_hash["syllables"]
  return [] if values.nil?

  unless values.is_a?(Array)
    raise ArgumentError, "#{path}: syllables must be an Array, got #{values.inspect}"
  end

  seen_verses = []
  values.each_with_index.map do |value, index|
    syllable(value, seen_verses, "#{path}.syllables[#{index}]")
  end
end

#position(value, path) ⇒ Object

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.



12
13
14
15
16
17
18
19
# File 'lib/head_music/content/composition/schema_values.rb', line 12

def 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

#rhythmic_value(value, path) ⇒ Object



51
52
53
54
55
56
57
# File 'lib/head_music/content/composition/schema_values.rb', line 51

def 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

#sound(value, path) ⇒ Object (private)

Raises:

  • (ArgumentError)


144
145
146
147
148
149
# File 'lib/head_music/content/composition/schema_values.rb', line 144

def sound(value, path)
  return pitch(value, path) if value.is_a?(String)
  return unpitched_sound(value, path) if value.is_a?(Hash)

  raise ArgumentError, "#{path}: unknown sound #{value.inspect}"
end

#syllable(value, seen_verses, path) ⇒ Object (private)

Raises:

  • (ArgumentError)


123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
# File 'lib/head_music/content/composition/schema_values.rb', line 123

def syllable(value, seen_verses, path)
  unless value.is_a?(Hash)
    raise ArgumentError, "#{path}: syllable must be a Hash, got #{value.inspect}"
  end

  text = value["text"]
  unless text.is_a?(String) && !text.empty?
    raise ArgumentError, "#{path}: syllable text must be a non-empty String, got #{text.inspect}"
  end

  verse = value.fetch("verse", 1)
  unless verse.is_a?(Integer) && verse.positive?
    raise ArgumentError, "#{path}: verse must be a positive Integer, got #{verse.inspect}"
  end

  raise ArgumentError, "#{path}: duplicate verse #{verse}" if seen_verses.include?(verse)
  seen_verses << verse

  HeadMusic::Content::Syllable.from_h(value)
end

#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.

Raises:

  • (ArgumentError)


154
155
156
157
158
159
160
161
162
163
164
165
# File 'lib/head_music/content/composition/schema_values.rb', line 154

def 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

#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.

Returns:

  • (Boolean)


106
107
108
109
110
111
112
# File 'lib/head_music/content/composition/schema_values.rb', line 106

def valid_rhythmic_value?(rhythmic_value)
  return false unless rhythmic_value.is_a?(HeadMusic::Rudiment::RhythmicValue)
  return false unless rhythmic_value.unit

  tied_value = rhythmic_value.tied_value
  tied_value.nil? || valid_rhythmic_value?(tied_value)
end