Class: Praxis::MediaTypeIdentifier

Inherits:
Attributor::Model
  • Object
show all
Defined in:
lib/praxis/media_type_identifier.rb

Overview

Ruby object representation of an Internet Media Type Identifier as defined by RFC6838; also known as MIME types due to their origin in RFC2046 (the MIME specification).

Constant Summary collapse

VALID_TYPE =

Postel’s principle encourages us to accept anything that MIGHT be an identifier, although the syntax for type identifiers is substantially narrower than what we accept there.

Note that this ONLY matches type, subtype and suffix; we handle options differently.

%r{^\s*(?<type>[^/]+)/(?<subtype>[^+]+)(?<nothing>\+(?<suffix>[^; ]+))?\s*$}x.freeze
PARAMETER_SEPARATOR =

Pattern that separates parameters of a media type from each other, and from the base identifier.

/\s*;\s*/x.freeze
WORD_SEPARATOR =

Pattern used to identify the first “word” when we encounter a malformed type identifier, so we can apply a heuristic and assume the user meant “application/XYZ”.

/[^a-z0-9-]/i.freeze
QUOTED_STRING =

Pattern that lets us strip quotes from parameter values.

/^".*"$/.freeze
WILDCARD =

Token that indicates a media-type component that matches anything.

'*'
Parameters =

Inner type representing semicolon-delimited parameters.

Attributor::Hash.of(key: String)

Class Method Summary collapse

Instance Method Summary collapse

Class Method Details

.load(value, context = Attributor::DEFAULT_ROOT_CONTEXT, recurse: false, **options) ⇒ MediaTypeIdentifier

Parse a media type identifier from a String, or load it from a Hash or Model. Assume malformed types represent a subtype, e.g. “nachos” => application/nachos“

Parameters:

  • value (String, Hash, Attributor::Model)

Returns:

See Also:

  • Attributor::Model#load


46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
# File 'lib/praxis/media_type_identifier.rb', line 46

def self.load(value, context = Attributor::DEFAULT_ROOT_CONTEXT, recurse: false, **options)
  case value
  when ::String
    return nil if value.blank?

    base, *parameters = value.split(PARAMETER_SEPARATOR)
    match = VALID_TYPE.match(base)

    obj = new
    if match
      parameters = parameters.each_with_object({}) do |e, h|
        k, v = e.split('=', 2)
        v = v[1...-1] if v =~ QUOTED_STRING
        h[k] = v
      end

      obj.type = match[:type]
      obj.subtype = match[:subtype]
      obj.suffix = match[:suffix]
      obj.parameters = parameters
    else
      obj.type = 'application'
      obj.subtype = base.split(WORD_SEPARATOR, 2).first
      obj.suffix = ::String.new
      obj.parameters = {}
    end
    obj
  when nil
    nil
  else
    super
  end
end

Instance Method Details

#+(other) ⇒ MediaTypeIdentifier

Extend this type identifier by adding a suffix or parameter(s); return a new type identifier.

Examples:

Indicate JSON structured syntax

MediaTypeIdentifier.new('application/vnd.widget') + 'json' # => 'application/vnd.widget+json'

Indicate UTF8 encoding

MediaTypeIdentifier.new('application/vnd.widget') + 'charset=UTF8' # => 'application/vnd.widget; charset="UTF8"'

Parameters:

  • extension (String)

    an optional suffix, followed by an optional semicolon-separated list of name=“value” pairs

Returns:

Raises:

  • (ArgumentError)

    when an invalid string is passed (e.g. containing neither parameters nor a suffix)



184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
# File 'lib/praxis/media_type_identifier.rb', line 184

def +(other)
  parameters = other.split(PARAMETER_SEPARATOR)
  # remove useless initial '; '
  parameters.shift if parameters.first && parameters.first.empty?

  raise ArgumentError, 'Must pass a type identifier suffix and/or parameters' if parameters.empty?

  suffix = parameters.shift unless parameters.first.include?('=')
  # remove redundant '+'
  suffix = suffix[1..] if suffix && suffix[0] == '+'

  parameters = parameters.each_with_object({}) do |e, h|
    k, v = e.split('=', 2)
    v = v[1...-1] if v =~ /^".*"$/
    h[k] = v
    h
  end
  parameters = Parameters.load(parameters)

  obj = self.class.new
  obj.type = type
  obj.subtype = subtype
  target_suffix = suffix || self.suffix
  obj.suffix = redundant_suffix(target_suffix) ? ::String.new : target_suffix
  obj.parameters = self.parameters.merge(parameters)

  obj
end

#=~(other) ⇒ Boolean

Determine whether this type is compatible with (i.e. is a subset or specialization of) another identifier. This is the same operation as #match, but with the position of the operands switched – analogous to “Regexp#match(String)” vs “String =~ Regexp”.

Parameters:

Returns:

  • (Boolean)

    true if this type is compatible with other, false otherwise

See Also:



122
123
124
# File 'lib/praxis/media_type_identifier.rb', line 122

def =~(other)
  other.match(self)
end

#handler_nameString

Make an educated guess about the structured-syntax encoding implied by this media type, which in turn governs which handler should be used to parse and generate media of this type.

If a suffix e.g. “+json” is present, return it. Otherwise, return the subtype.

Returns:

  • (String)

    a type identifier fragment e.g. “json” or “xml” that MAY indicate encoding

See Also:

  • xxx


168
169
170
# File 'lib/praxis/media_type_identifier.rb', line 168

def handler_name
  suffix.empty? ? subtype : suffix
end

#match(other) ⇒ Boolean

Determine whether another identifier is compatible with (i.e. is a subset or specialization of) this identifier.

Examples:

match anything

MediaTypeIdentifier.load('*/*').match('application/icecream+cone; flavor=vanilla') # => true

match a subtype wildcard

MediaTypeIdentifier.load('image/*').match('image/jpeg') # => true

match a specific type irrespective of structured syntax

MediaTypeIdentifier.load('application/vnd.widget').match('application/vnd.widget+json') # => true

match a specific type, respective of important parameters but irrespective of extra parameters or structured syntax

MediaTypeIdentifier.load('application/vnd.widget; type=collection').match('application/vnd.widget+json; material=steel; type=collection') # => true

Parameters:

Returns:

  • (Boolean)

    true if this type is compatible with other, false otherwise



98
99
100
101
102
103
104
105
106
107
108
109
110
111
# File 'lib/praxis/media_type_identifier.rb', line 98

def match(other)
  other = MediaTypeIdentifier.load(other)

  return false if other.nil?
  return false unless type == other.type || type == WILDCARD
  return false unless subtype == other.subtype || subtype == WILDCARD
  return false unless suffix.empty? || suffix == other.suffix

  parameters.each_pair do |k, v|
    return false unless other.parameters[k] == v
  end

  true
end

#redundant_suffix(suffix) ⇒ Object



213
214
215
216
217
218
219
# File 'lib/praxis/media_type_identifier.rb', line 213

def redundant_suffix(suffix)
  # application/json does not need to be suffixed with +json (same for application/xml)
  # we're supporting text/json and text/xml for older formats as well
  return true if (type == 'application' || type == 'text') && subtype == suffix

  false
end

#to_sString Also known as: to_str

Returns canonicalized representation of the media type including all suffixes and parameters.

Returns:

  • (String)

    canonicalized representation of the media type including all suffixes and parameters



127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
# File 'lib/praxis/media_type_identifier.rb', line 127

def to_s
  # Our handcrafted media types consist of a rich chocolatey center
  s = ::String.new("#{type}/#{subtype}")

  # coated in a hard candy shell
  s << '+' << suffix unless suffix.empty?

  # and encrusted with lexically-ordered sprinkles
  unless parameters.empty?
    s << '; '
    s << parameters.keys.sort.map { |k| "#{k}=#{parameters[k]}" }.join('; ')
  end

  # May contain peanuts, tree nuts, soy, dairy, sawdust or glue
  s
end

#without_parametersMediaTypeIdentifier

If parameters are empty, return self; otherwise return a new object consisting of this type minus parameters.

Returns:



150
151
152
153
154
155
156
157
# File 'lib/praxis/media_type_identifier.rb', line 150

def without_parameters
  if parameters.empty?
    self
  else
    val = { type: type, subtype: subtype, suffix: suffix }
    MediaTypeIdentifier.load(val)
  end
end