Module: Audioproxy::Expiry

Defined in:
lib/audioproxy/expiry.rb

Overview

Turns the two Rails-shaped expiry spellings — expires_in: 1.hour and expires_at: some_time — into the single Integer of unix seconds the proxy's exp option takes.

Every method here raises rather than coercing. The proxy answers a URL whose exp has passed with 410 and one out of bounds with 422, both at request time and neither anywhere near the call that built it, so an input this module cannot read exactly is an error at the call site (D5).

Constant Summary collapse

UNSET =

Sentinel for "this keyword was not passed". nil cannot do the job: it is the documented per-call opt-out from a configured default (D6).

Object.new.freeze

Class Method Summary collapse

Class Method Details

.seconds(value, source:) ⇒ Object

A window of whole positive seconds, as an Integer. Shared by the keyword and by Config#expires_in= so that a value accepted at boot is exactly a value accepted per call.



42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
# File 'lib/audioproxy/expiry.rb', line 42

def seconds(value, source:)
  case value
  # #value is the exact number of seconds the Duration stands for (3600
  # for 1.hour), and it stays an Integer where the caller wrote one, so a
  # far-future window does not lose digits to a double on the way past.
  when ActiveSupport::Duration then whole_seconds(value, value.value, source: source)
  when Integer then value
  else
    raise ArgumentError,
      "Audioproxy #{source} must be an ActiveSupport::Duration or an Integer of seconds, " \
      "got #{value.inspect}"
  end.tap do |seconds|
    unless seconds.positive?
      raise ArgumentError,
        "Audioproxy #{source} must be a positive number of seconds, got #{value.inspect}; " \
        "a window of #{seconds} mints a URL that is already expired"
    end
  end
end

.timestamp(expires_in:, expires_at:, default_expires_in:, now:) ⇒ Object

The exp value for one URL, or nil for no expiry at all. now is supplied by the caller and read once per URL, so the arithmetic below and the past-check cannot straddle a second boundary (D4).



23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
# File 'lib/audioproxy/expiry.rb', line 23

def timestamp(expires_in:, expires_at:, default_expires_in:, now:)
  if given?(expires_in) && given?(expires_at)
    raise ArgumentError,
      "Audioproxy url_for takes either expires_in: or expires_at:, not both; " \
      "expires_in: is a window from now, expires_at: is the instant itself"
  end

  return at(expires_at, now: now) if given?(expires_at)
  return within(expires_in, now: now, source: "url_for expires_in:") if given?(expires_in)
  return nil if default_expires_in.nil?

  # Already validated at assignment, so this is arithmetic and nothing
  # else — but it still goes through the bound check that `within` runs.
  within(default_expires_in, now: now, source: "config expires_in")
end