Class: Ur::ContentType

Inherits:
String
  • Object
show all
Defined in:
lib/ur/content_type.rb

Overview

Ur::ContentType represents a Content-Type header field. it parses the media type and its components, as well as any parameters.

this class aims to be permissive in what it will parse. it will not raise any error when given a malformed or syntactically invalid Content-Type string. fields and parameters parsed from invalid Content-Type strings are undefined, but this class generally tries to make the most sense of what it's given.

this class is based on RFCs:

Constant Summary collapse

MEDIA_TYPE_REGEXP =

the character ranges in this SHOULD be significantly more restrictive, and the /<subtype> construct should not be optional. however, we'll aim to match whatever media type we are given.

example:

MEDIA_TYPE_REGEXP.match('application/vnd.github+json').named_captures
=>
{
  "media_type" => "application/vnd.github+json",
  "type" => "application",
  "subtype" => "vnd.github+json",
  "facet" => "vnd",
  "suffix" => "json",
}

example of being more permissive than the spec allows:

MEDIA_TYPE_REGEXP.match('where the %$*! am I').named_captures
=>
{
  "media_type" => "where the %$*! am I",
  "type" => "where the %$*! am I",
  "subtype" => nil,
  "facet" => nil,
  "suffix" => nil
}
%r{
  (?<media_type>       # the media type includes the type and subtype
    (?<type>[^\/;\"]*) # the type precedes the first slash
    (?:\/              # slash
      (?<subtype>      # the subtype includes the facet, the suffix, and bits in between
        (?:
          (?<facet>[^.+;\"]*) # the facet name comes before the first . in the subtype
          \.             # dot
        )?
        [^\+;\"]*      # anything between facet and suffix
        (?:\+          # plus
          (?<suffix>[^;\"]*) # optional suffix
        )?
      )
    )? # the subtype should not be optional, but we will match a type without subtype anyway
  )
}x
SOME_TEXT_SUBTYPES =
%w(
  x-www-form-urlencoded
  json
  json-seq
  jwt
  jose
  yaml
  x-yaml
  xml
  html
  css
  javascript
  ecmascript
).map(&:freeze).freeze

Instance Attribute Summary collapse

Instance Method Summary collapse

Constructor Details

#initialize(*a) ⇒ ContentType

Returns a new instance of ContentType.



72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
# File 'lib/ur/content_type.rb', line 72

def initialize(*a)
  super

  scanner = StringScanner.new(self)

  if scanner.scan(MEDIA_TYPE_REGEXP)
    @media_type = scanner[:media_type].strip.freeze if scanner[:media_type]
    @type      = scanner[:type].strip.freeze       if scanner[:type]
    @subtype  = scanner[:subtype].strip.freeze    if scanner[:subtype]
    @facet   = scanner[:facet].strip.freeze      if scanner[:facet]
    @suffix = scanner[:suffix].strip.freeze     if scanner[:suffix]
  end

  @parameters = Hash.new do |h, k|
    if k.respond_to?(:downcase) && k != k.downcase
      h[k.downcase]
    else
      nil
    end
  end

  while scanner.scan(/(;\s*)+/)
    key = scanner.scan(/[^;=\"]*/)
    if key && scanner.scan(/=/)
      value = String.new
      until scanner.eos? || scanner.check(/;/)
        if scanner.scan(/\s+/)
          ws = scanner[0]
          # discard trailing whitespace.
          # other whitespace isn't technically valid but we are permissive so we put it in the value.
          value << ws unless scanner.eos? || scanner.check(/;/)
        elsif scanner.scan(/"/)
          until scanner.eos? || scanner.scan(/"/)
            if scanner.scan(/\\/)
              value << scanner.getch unless scanner.eos?
            end
            value << scanner.scan(/[^\"\\]*/)
          end
        else
          value << scanner.scan(/[^\s;\"]*/)
        end
      end
      @parameters[key.downcase.freeze] = value.freeze
    end
  end

  @parameters.freeze

  freeze
end

Instance Attribute Details

#facetString? (readonly)

the 'facet' portion of our media type. e.g. "vnd" in content-type: application/vnd.github+json; charset="utf-8"

Returns:

  • (String, nil)


141
142
143
# File 'lib/ur/content_type.rb', line 141

def facet
  @facet
end

#media_typeString? (readonly)

the media type of this content type. e.g. "application/vnd.github+json" in content-type: application/vnd.github+json; charset="utf-8"

Returns:

  • (String, nil)


126
127
128
# File 'lib/ur/content_type.rb', line 126

def media_type
  @media_type
end

#parametersHash<String, String> (readonly)

parameters of this content type. e.g. {"charset" => "utf-8"} in content-type: application/vnd.github+json; charset="utf-8"

Returns:

  • (Hash<String, String>)


151
152
153
# File 'lib/ur/content_type.rb', line 151

def parameters
  @parameters
end

#subtypeString? (readonly)

the 'subtype' portion of our media type. e.g. "vnd.github+json" in content-type: application/vnd.github+json; charset="utf-8"

Returns:

  • (String, nil)


136
137
138
# File 'lib/ur/content_type.rb', line 136

def subtype
  @subtype
end

#suffixString? (readonly)

the 'suffix' portion of our media type. e.g. "json" in content-type: application/vnd.github+json; charset="utf-8"

Returns:

  • (String, nil)


146
147
148
# File 'lib/ur/content_type.rb', line 146

def suffix
  @suffix
end

#typeString? (readonly)

the 'type' portion of our media type. e.g. "application" in content-type: application/vnd.github+json; charset="utf-8"

Returns:

  • (String, nil)


131
132
133
# File 'lib/ur/content_type.rb', line 131

def type
  @type
end

Instance Method Details

#binary?(unknown: true) ⇒ Boolean

does this content type appear to be binary? this library makes its best guess based on a very incomplete knowledge of which media types indicate binary or text.

Parameters:

  • unknown (Boolean) (defaults to: true)

    return this value when we have no idea whether our media type is binary or text.

Returns:

  • (Boolean)


195
196
197
198
199
200
201
202
203
204
205
206
207
# File 'lib/ur/content_type.rb', line 195

def binary?(unknown: true)
  return false if type_text?

  SOME_TEXT_SUBTYPES.each do |cmpsubtype|
    return false if (suffix ? suffix.casecmp?(cmpsubtype) : subtype ? subtype.casecmp?(cmpsubtype) : false)
  end

  # these are generally binary
  return true if type_image? || type_audio? || type_video?

  # we're out of ideas
  return unknown
end

#form_urlencoded?Boolean

is this a x-www-form-urlencoded content type?

Returns:

  • (Boolean)


223
224
225
# File 'lib/ur/content_type.rb', line 223

def form_urlencoded?
  suffix ? suffix.casecmp?('x-www-form-urlencoded') : subtype ? subtype.casecmp?('x-www-form-urlencoded') : false
end

#json?Boolean

is this a JSON content type?

Returns:

  • (Boolean)


211
212
213
# File 'lib/ur/content_type.rb', line 211

def json?
  suffix ? suffix.casecmp?('json') : subtype ? subtype.casecmp?('json') : false
end

#subtype?(other_subtype) ⇒ Boolean

is the 'subtype' portion of our media type equal (case-insensitive) to the given other_subtype

Parameters:

  • other_subtype

Returns:

  • (Boolean)


163
164
165
# File 'lib/ur/content_type.rb', line 163

def subtype?(other_subtype)
  subtype ? subtype.casecmp?(other_subtype) : false
end

#suffix?(other_suffix) ⇒ Boolean

is the 'suffix' portion of our media type equal (case-insensitive) to the given other_suffix

Parameters:

  • other_suffix

Returns:

  • (Boolean)


170
171
172
# File 'lib/ur/content_type.rb', line 170

def suffix?(other_suffix)
  suffix ? suffix.casecmp?(other_suffix) : false
end

#type?(other_type) ⇒ Boolean

is the 'type' portion of our media type equal (case-insensitive) to the given other_type

Parameters:

  • other_type

Returns:

  • (Boolean)


156
157
158
# File 'lib/ur/content_type.rb', line 156

def type?(other_type)
  type ? type.casecmp?(other_type) : false
end

#type_application?Boolean

is the 'type' portion of our media type 'application'

Returns:

  • (Boolean)


253
254
255
# File 'lib/ur/content_type.rb', line 253

def type_application?
  type ? type.casecmp?('application') : false
end

#type_audio?Boolean

is the 'type' portion of our media type 'audio'

Returns:

  • (Boolean)


241
242
243
# File 'lib/ur/content_type.rb', line 241

def type_audio?
  type ? type.casecmp?('audio') : false
end

#type_image?Boolean

is the 'type' portion of our media type 'image'

Returns:

  • (Boolean)


235
236
237
# File 'lib/ur/content_type.rb', line 235

def type_image?
  type ? type.casecmp?('image') : false
end

#type_message?Boolean

is the 'type' portion of our media type 'message'

Returns:

  • (Boolean)


259
260
261
# File 'lib/ur/content_type.rb', line 259

def type_message?
  type ? type.casecmp?('message') : false
end

#type_multipart?Boolean

is the 'type' portion of our media type 'multipart'

Returns:

  • (Boolean)


265
266
267
# File 'lib/ur/content_type.rb', line 265

def type_multipart?
  type ? type.casecmp?('multipart') : false
end

#type_text?Boolean

is the 'type' portion of our media type 'text'

Returns:

  • (Boolean)


229
230
231
# File 'lib/ur/content_type.rb', line 229

def type_text?
  type ? type.casecmp?('text') : false
end

#type_video?Boolean

is the 'type' portion of our media type 'video'

Returns:

  • (Boolean)


247
248
249
# File 'lib/ur/content_type.rb', line 247

def type_video?
  type ? type.casecmp?('video') : false
end

#xml?Boolean

is this an XML content type?

Returns:

  • (Boolean)


217
218
219
# File 'lib/ur/content_type.rb', line 217

def xml?
  suffix ? suffix.casecmp?('xml') : subtype ? subtype.casecmp?('xml') : false
end