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 / 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.



68
69
70
71
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
# File 'lib/ur/content_type.rb', line 68

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)

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

Returns:

  • (String, nil)

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



133
134
135
# File 'lib/ur/content_type.rb', line 133

def facet
  @facet
end

#media_typeString? (readonly)

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

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



121
122
123
# File 'lib/ur/content_type.rb', line 121

def media_type
  @media_type
end

#parametersHash<String, String> (readonly)

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

Returns:

  • (Hash<String, String>)

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



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

def parameters
  @parameters
end

#subtypeString? (readonly)

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

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



129
130
131
# File 'lib/ur/content_type.rb', line 129

def subtype
  @subtype
end

#suffixString? (readonly)

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

Returns:

  • (String, nil)

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



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

def suffix
  @suffix
end

#typeString? (readonly)

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

Returns:

  • (String, nil)

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



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

def type
  @type
end

Instance Method Details

#binary?(unknown: true) ⇒ Boolean

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

    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.



181
182
183
184
185
186
187
188
189
190
191
192
193
# File 'lib/ur/content_type.rb', line 181

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

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

Returns:

  • (Boolean)

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



206
207
208
# File 'lib/ur/content_type.rb', line 206

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

#json?Boolean

Returns is this a JSON content type?.

Returns:

  • (Boolean)

    is this a JSON content type?



196
197
198
# File 'lib/ur/content_type.rb', line 196

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

#subtype?(other_subtype) ⇒ Boolean

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

Parameters:

  • other_subtype

Returns:

  • (Boolean)

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



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

def subtype?(other_subtype)
  subtype && subtype.casecmp?(other_subtype)
end

#suffix?(other_suffix) ⇒ Boolean

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

Parameters:

  • other_suffix

Returns:

  • (Boolean)

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



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

def suffix?(other_suffix)
  suffix && suffix.casecmp?(other_suffix)
end

#type?(other_type) ⇒ Boolean

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

Parameters:

  • other_type

Returns:

  • (Boolean)

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



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

def type?(other_type)
  type && type.casecmp?(other_type)
end

#type_application?Boolean

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

Returns:

  • (Boolean)

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



231
232
233
# File 'lib/ur/content_type.rb', line 231

def type_application?
  type && type.casecmp?('application')
end

#type_audio?Boolean

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

Returns:

  • (Boolean)

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



221
222
223
# File 'lib/ur/content_type.rb', line 221

def type_audio?
  type && type.casecmp?('audio')
end

#type_image?Boolean

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

Returns:

  • (Boolean)

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



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

def type_image?
  type && type.casecmp?('image')
end

#type_message?Boolean

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

Returns:

  • (Boolean)

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



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

def type_message?
  type && type.casecmp?('message')
end

#type_multipart?Boolean

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

Returns:

  • (Boolean)

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



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

def type_multipart?
  type && type.casecmp?('multipart')
end

#type_text?Boolean

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

Returns:

  • (Boolean)

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



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

def type_text?
  type && type.casecmp?('text')
end

#type_video?Boolean

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

Returns:

  • (Boolean)

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



226
227
228
# File 'lib/ur/content_type.rb', line 226

def type_video?
  type && type.casecmp?('video')
end

#xml?Boolean

Returns is this an XML content type?.

Returns:

  • (Boolean)

    is this an XML content type?



201
202
203
# File 'lib/ur/content_type.rb', line 201

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