Module: Audioproxy::Options

Defined in:
lib/audioproxy/options.rb

Overview

Renders the proxy's option grammar: +/+-separated key:value segments, with colon-separated parts for the multi-part keys.

This layer renders, it does not validate (D1). Value domains and cross-key rules belong to the proxy, which versions them with the server and returns structured 422s. The exceptions are values this module cannot render faithfully — an unknown key, a number that does not fit the grammar, a value carrying a separator — because a mangled segment is a valid-looking URL for the wrong variant, and it fails at request time, far from here.

Constant Summary collapse

KEYS =

The proxy's fourteen option keys, canonical short spellings.

%i[bd br cb ch dl f fade gain norm pk_fmt pts q sr t].freeze
ALIASES =

A spelled-out spelling for each canonical key, for call sites that would rather read than decode. Total over KEYS, so "does this key have an alias" never has two answers: fade and gain are already words and alias to themselves. The names are the proxy's own where it has one — its Options struct calls pts peak_count and pk_fmt peak_format — so this is one vocabulary spelled twice, not a second vocabulary (D2).

{
  f: :format,
  br: :bitrate,
  q: :quality,
  sr: :sample_rate,
  ch: :channels,
  bd: :bit_depth,
  t: :trim,
  fade: :fade,
  gain: :gain,
  norm: :normalize,
  pts: :peak_count,
  pk_fmt: :peak_format,
  dl: :download,
  cb: :cache_buster
}.freeze
CANONICAL =

Every accepted spelling to the canonical key it renders as. Canonical keys map to themselves, so resolution is one lookup rather than a conditional.

ALIASES.each_with_object({}) { |(key, spelled), table| table[spelled] = key }
.merge(KEYS.to_h { |key| [ key, key ] })
.freeze
MULTI_PART_KEYS =

Keys whose grammar takes colon-separated parts: t:START[:DURATION], fade:IN[:OUT], norm:ebu[:I[:TP[:LRA]]].

%i[t fade norm].freeze
TIME_KEYS =

Keys whose values are a number of seconds, and so may be written as an ActiveSupport::Duration (D6).

%i[t fade].freeze
OPAQUE_KEYS =

Keys the proxy treats as opaque payloads (download filename, cache buster), rendered verbatim rather than number-formatted.

%i[cb dl].freeze
MAX_DECIMALS =

The proxy caps decimals at 3 places when it parses, and hashes the normalized options string into its cache key.

3
SEPARATORS =

Characters a rendered value may not carry. The builder supplies '/' and ':', so a value containing either silently invents a segment or a part. '?' and '#' end the path as far as a browser is concerned, which truncates what the proxy receives below what was signed: a 403 at request time, far from the call. Whitespace and control characters are not URL bytes at all.

%r{[/:?#\s]|[[:cntrl:]]}

Class Method Summary collapse

Class Method Details

.format_number(value) ⇒ Object

The proxy's canonical minimal number spelling. Strings and symbols pass through untouched — the caller opted out of formatting.



119
120
121
122
123
124
125
126
127
128
129
130
131
132
# File 'lib/audioproxy/options.rb', line 119

def format_number(value)
  case value
  when String then value
  when Symbol then value.to_s
  when Integer then value.to_s
  when Complex
    # Numeric, but Complex#round is undefined and Complex#to_r silently
    # drops a zero imaginary part. Neither is a number this grammar has.
    raise ArgumentError, "Audioproxy option values must be real numbers, got #{value.inspect}"
  when Numeric then format_decimal(value)
  else
    raise ArgumentError, "Audioproxy option values must be numbers, strings or symbols, got #{value.class}"
  end
end

.render(options) ⇒ Object

Renders an ordered key => value Hash into an options segment. Caller order is preserved (D4); normalization is the proxy's business.



73
74
75
# File 'lib/audioproxy/options.rb', line 73

def render(options)
  resolve(options).map { |key, value| segment(key, value) }.join("/")
end

.resolve(options) ⇒ Object

Rewrites a key => value Hash onto the canonical short keys, so that everything downstream — rendering, ordering, the defaults merge — sees one vocabulary and is unaware aliases exist (D1). Insertion order is preserved, so an aliased key keeps its slot. Unrecognized keys pass through untouched, to be reported by segment against the key table rather than by a second, thinner error here.



83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
# File 'lib/audioproxy/options.rb', line 83

def resolve(options)
  spellings = {}

  options.each_with_object({}) do |(key, value), resolved|
    canonical = CANONICAL.fetch(symbolize(key), key)

    # Ruby's keyword collection keeps both spellings, and picking a winner
    # by position would make the URL depend on argument order in a way
    # nothing else here does (D4).
    # inspect, not interpolation: the two spellings are often identical as
    # text — "fade" and :fade, or a self-aliasing key given both ways —
    # and "as fade and fade" tells the caller nothing.
    if (first = spellings[canonical])
      raise ArgumentError,
        "Audioproxy option #{canonical} was given twice, as #{first.inspect} and #{key.inspect}; " \
        "each option takes one spelling per call"
    end
    spellings[canonical] = key

    resolved[canonical] = value
  end
end

.segment(key, value) ⇒ Object

Renders one key:value segment. The key may be canonical or an alias.



107
108
109
110
111
112
113
114
115
# File 'lib/audioproxy/options.rb', line 107

def segment(key, value)
  key = CANONICAL.fetch(symbolize(key)) do |unknown|
    raise ArgumentError,
      "unknown Audioproxy option #{unknown.inspect}; known keys are #{KEYS.join(", ")}, " \
      "each also accepted as its spelled-out alias (#{ALIASES[:br]}, #{ALIASES[:sr]}, #{ALIASES[:pk_fmt]}, …)"
  end

  "#{key}:#{render_value(key, value)}"
end