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



74
75
76
77
78
79
80
# File 'lib/head_music/content/composition/schema_values.rb', line 74

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)


97
98
99
100
101
102
# File 'lib/head_music/content/composition/schema_values.rb', line 97

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

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


104
105
106
107
108
109
# File 'lib/head_music/content/composition/schema_values.rb', line 104

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

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


114
115
116
117
118
119
120
121
122
123
124
125
# File 'lib/head_music/content/composition/schema_values.rb', line 114

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)


87
88
89
90
91
92
93
# File 'lib/head_music/content/composition/schema_values.rb', line 87

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