ur 0.0.4 → 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/CHANGELOG.md +4 -0
- data/README.md +2 -2
- data/lib/ur.rb +12 -45
- data/lib/ur/content_type.rb +243 -0
- data/lib/ur/{processing.rb → metadata.rb} +1 -1
- data/lib/ur/request_and_response.rb +17 -7
- data/lib/ur/version.rb +1 -1
- data/resources/ur.schema.yml +45 -0
- data/test/content_type_test.rb +342 -0
- data/test/test_helper.rb +11 -0
- data/test/ur_metadata_test.rb +11 -0
- data/test/ur_rack_test.rb +4 -4
- data/test/ur_test.rb +12 -12
- metadata +9 -6
- data/lib/ur/content_type_attrs.rb +0 -83
- data/test/ur_processing_test.rb +0 -11
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 551019c356f6c251fc9330e21d58be49983302fa74754598d44a5ce02c8b6fec
|
4
|
+
data.tar.gz: 06e531326d20440cf145328ae17f07251053aeb5a4af3ecfbf8d925620b7e89a
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: d68634fbde7ef1511914392857f6248db0a8bba6f18a044b262da2d10912c5059d4eb08c3953d1cc18d2a4fbd8b27bc6face500211b2fbb8c1eef85083aecd84
|
7
|
+
data.tar.gz: dbf33cc387daf9d82181330f029335a89c179582d614c2d77d07359c806203aebaa2e2c86fe785f568529f0cb2cdb255a74f37e623404f7335fd5da40f4cb175
|
data/CHANGELOG.md
CHANGED
data/README.md
CHANGED
@@ -4,13 +4,13 @@ Ur: Unified Request/Response Representation in Ruby
|
|
4
4
|
|
5
5
|
## Properties
|
6
6
|
|
7
|
-
An ur primarily consists of a request, a response, and additional
|
7
|
+
An ur primarily consists of a request, a response, and additional metadata.
|
8
8
|
|
9
9
|
The request consists of the request method, uri, headers, and body.
|
10
10
|
|
11
11
|
The response consists of the response status, headers, and body.
|
12
12
|
|
13
|
-
The
|
13
|
+
The metadata consist of the time the request began, the duration of the request, or tag strings. This is optional.
|
14
14
|
|
15
15
|
Other attributes may be present, and are ignored by this library.
|
16
16
|
|
data/lib/ur.rb
CHANGED
@@ -3,43 +3,10 @@ require "ur/version"
|
|
3
3
|
require 'jsi'
|
4
4
|
require 'time'
|
5
5
|
require 'addressable/uri'
|
6
|
+
require 'pathname'
|
6
7
|
|
7
|
-
|
8
|
-
|
9
|
-
type: 'object',
|
10
|
-
properties: {
|
11
|
-
bound: {
|
12
|
-
type: 'string',
|
13
|
-
description: %q([rfc2616] Inbound and outbound refer to the request and response paths for messages: "inbound" means "traveling toward the origin server", and "outbound" means "traveling toward the user agent"),
|
14
|
-
enum: ['inbound', 'outbound'],
|
15
|
-
},
|
16
|
-
request: {
|
17
|
-
type: 'object',
|
18
|
-
properties: {
|
19
|
-
method: {type: 'string', description: 'HTTP ', example: 'POST'},
|
20
|
-
uri: {type: 'string', example: 'https://example.com/foo?bar=baz'},
|
21
|
-
headers: {type: 'object'},
|
22
|
-
body: {type: 'string'},
|
23
|
-
},
|
24
|
-
},
|
25
|
-
response: {
|
26
|
-
type: 'object',
|
27
|
-
properties: {
|
28
|
-
status: {type: 'integer', example: 200},
|
29
|
-
headers: {type: 'object'},
|
30
|
-
body: {type: 'string'},
|
31
|
-
},
|
32
|
-
},
|
33
|
-
processing: {
|
34
|
-
type: 'object',
|
35
|
-
properties: {
|
36
|
-
began_at_s: {type: 'string'},
|
37
|
-
duration: {type: 'number'},
|
38
|
-
tags: {type: 'array', items: {type: 'string'}}
|
39
|
-
},
|
40
|
-
},
|
41
|
-
},
|
42
|
-
})
|
8
|
+
UR_ROOT = Pathname.new(__FILE__).dirname.parent.expand_path
|
9
|
+
Ur = JSI.class_for_schema(YAML.load_file(UR_ROOT.join('resources/ur.schema.yml')))
|
43
10
|
class Ur
|
44
11
|
VERSION = UR_VERSION
|
45
12
|
|
@@ -52,12 +19,12 @@ class Ur
|
|
52
19
|
|
53
20
|
Request = JSI.class_for_schema(self.schema['properties']['request'])
|
54
21
|
Response = JSI.class_for_schema(self.schema['properties']['response'])
|
55
|
-
|
22
|
+
Metadata = JSI.class_for_schema(self.schema['properties']['metadata'])
|
56
23
|
require 'ur/request'
|
57
24
|
require 'ur/response'
|
58
|
-
require 'ur/
|
25
|
+
require 'ur/metadata'
|
59
26
|
|
60
|
-
autoload :
|
27
|
+
autoload :ContentType, 'ur/content_type'
|
61
28
|
|
62
29
|
class << self
|
63
30
|
def from_rack_request(request_env)
|
@@ -70,7 +37,7 @@ class Ur
|
|
70
37
|
end
|
71
38
|
|
72
39
|
new({'bound' => 'inbound'}).tap do |ur|
|
73
|
-
ur.
|
40
|
+
ur.metadata.begin!
|
74
41
|
|
75
42
|
ur.request['method'] = rack_request.request_method
|
76
43
|
|
@@ -103,7 +70,7 @@ class Ur
|
|
103
70
|
|
104
71
|
def from_faraday_request(request_env, logger: nil)
|
105
72
|
new({'bound' => 'outbound'}).tap do |ur|
|
106
|
-
ur.
|
73
|
+
ur.metadata.begin!
|
107
74
|
ur.request['method'] = request_env[:method].to_s
|
108
75
|
ur.request.uri = request_env[:url].normalize.to_s
|
109
76
|
ur.request.headers = request_env[:request_headers]
|
@@ -119,12 +86,12 @@ class Ur
|
|
119
86
|
end
|
120
87
|
self.request = {} if self.request.nil?
|
121
88
|
self.response = {} if self.response.nil?
|
122
|
-
self.
|
89
|
+
self.metadata = {} if self.metadata.nil?
|
123
90
|
end
|
124
91
|
|
125
92
|
def logger=(logger)
|
126
93
|
if logger && logger.formatter.respond_to?(:current_tags)
|
127
|
-
|
94
|
+
metadata.tags = logger.formatter.current_tags.dup
|
128
95
|
end
|
129
96
|
end
|
130
97
|
|
@@ -136,7 +103,7 @@ class Ur
|
|
136
103
|
response.body = response_body.to_enum.to_a.join('')
|
137
104
|
|
138
105
|
response_body_proxy = ::Rack::BodyProxy.new(response_body) do
|
139
|
-
|
106
|
+
metadata.finish!
|
140
107
|
|
141
108
|
yield
|
142
109
|
end
|
@@ -148,7 +115,7 @@ class Ur
|
|
148
115
|
response.status = response_env[:status]
|
149
116
|
response.headers = response_env[:response_headers]
|
150
117
|
response.set_body_from_faraday(response_env)
|
151
|
-
|
118
|
+
metadata.finish!
|
152
119
|
|
153
120
|
yield(response_env)
|
154
121
|
end
|
@@ -0,0 +1,243 @@
|
|
1
|
+
require 'ur' unless Object.const_defined?(:Ur)
|
2
|
+
|
3
|
+
class Ur
|
4
|
+
# Ur::ContentType represents a Content-Type header field.
|
5
|
+
# it parses the media type and its components, as well as any parameters.
|
6
|
+
#
|
7
|
+
# this class aims to be permissive in what it will parse. it will not raise any
|
8
|
+
# error when given a malformed or syntactically invalid Content-Type string.
|
9
|
+
# fields and parameters parsed from invalid Content-Type strings are undefined,
|
10
|
+
# but this class generally tries to make the most sense of what it's given.
|
11
|
+
#
|
12
|
+
# this class is based on RFCs:
|
13
|
+
# - Hypertext Transfer Protocol (HTTP/1.1): Semantics and Content
|
14
|
+
# Section 3.1.1.1. Media Type
|
15
|
+
# https://tools.ietf.org/html/rfc7231#section-3.1.1.1
|
16
|
+
# - Media Type Specifications and Registration Procedures https://tools.ietf.org/html/rfc6838
|
17
|
+
# - Multipurpose Internet Mail Extensions (MIME) Part One: Format of Internet Message Bodies.
|
18
|
+
# Section 5.1. Syntax of the Content-Type Header Field
|
19
|
+
# https://tools.ietf.org/html/rfc2045#section-5.1
|
20
|
+
# - Multipurpose Internet Mail Extensions (MIME) Part Two: Media Types
|
21
|
+
# https://tools.ietf.org/html/rfc2046
|
22
|
+
class ContentType < String
|
23
|
+
# the character ranges in this SHOULD be significantly more restrictive,
|
24
|
+
# and the /<subtype> construct should not be optional. however, we'll aim
|
25
|
+
# to match whatever media type we are given.
|
26
|
+
#
|
27
|
+
# example:
|
28
|
+
# MEDIA_TYPE_REGEXP.match('application/vnd.github+json').named_captures
|
29
|
+
# =>
|
30
|
+
# {
|
31
|
+
# "media_type" => "application/vnd.github+json",
|
32
|
+
# "type" => "application",
|
33
|
+
# "subtype" => "vnd.github+json",
|
34
|
+
# "facet" => "vnd",
|
35
|
+
# "suffix" => "json",
|
36
|
+
# }
|
37
|
+
#
|
38
|
+
# example of being more permissive than the spec allows:
|
39
|
+
# MEDIA_TYPE_REGEXP.match('where the %$*! am I').named_captures
|
40
|
+
# =>
|
41
|
+
# {
|
42
|
+
# "media_type" => "where the %$*! am I",
|
43
|
+
# "type" => "where the %$*! am I",
|
44
|
+
# "subtype" => nil,
|
45
|
+
# "facet" => nil,
|
46
|
+
# "suffix" => nil
|
47
|
+
# }
|
48
|
+
MEDIA_TYPE_REGEXP = %r{
|
49
|
+
(?<media_type> # the media type includes the type and subtype
|
50
|
+
(?<type>[^\/;\"]*) # the type precedes the first slash
|
51
|
+
(?:\/ # slash
|
52
|
+
(?<subtype> # the subtype includes the facet, the suffix, and bits in between
|
53
|
+
(?:
|
54
|
+
(?<facet>[^.+;\"]*) # the facet name comes before the first . in the subtype
|
55
|
+
\. # dot
|
56
|
+
)?
|
57
|
+
[^\+;\"]* # anything between facet and suffix
|
58
|
+
(?:\+ # plus
|
59
|
+
(?<suffix>[^;\"]*) # optional suffix
|
60
|
+
)?
|
61
|
+
)
|
62
|
+
)? # the subtype should not be optional, but we will match a type without subtype anyway
|
63
|
+
)
|
64
|
+
}x
|
65
|
+
|
66
|
+
def initialize(*a)
|
67
|
+
super
|
68
|
+
|
69
|
+
scanner = StringScanner.new(self)
|
70
|
+
|
71
|
+
if scanner.scan(MEDIA_TYPE_REGEXP)
|
72
|
+
@media_type = scanner[:media_type].strip.freeze if scanner[:media_type]
|
73
|
+
@type = scanner[:type].strip.freeze if scanner[:type]
|
74
|
+
@subtype = scanner[:subtype].strip.freeze if scanner[:subtype]
|
75
|
+
@facet = scanner[:facet].strip.freeze if scanner[:facet]
|
76
|
+
@suffix = scanner[:suffix].strip.freeze if scanner[:suffix]
|
77
|
+
end
|
78
|
+
|
79
|
+
@parameters = Hash.new do |h, k|
|
80
|
+
if k.respond_to?(:downcase) && k != k.downcase
|
81
|
+
h[k.downcase]
|
82
|
+
else
|
83
|
+
nil
|
84
|
+
end
|
85
|
+
end
|
86
|
+
|
87
|
+
while scanner.scan(/(;\s*)+/)
|
88
|
+
key = scanner.scan(/[^;=\"]*/)
|
89
|
+
if key && scanner.scan(/=/)
|
90
|
+
value = String.new
|
91
|
+
until scanner.eos? || scanner.check(/;/)
|
92
|
+
if scanner.scan(/\s+/)
|
93
|
+
ws = scanner[0]
|
94
|
+
# discard trailing whitespace.
|
95
|
+
# other whitespace isn't technically valid but we are permissive so we put it in the value.
|
96
|
+
value << ws unless scanner.eos? || scanner.check(/;/)
|
97
|
+
elsif scanner.scan(/"/)
|
98
|
+
until scanner.eos? || scanner.scan(/"/)
|
99
|
+
if scanner.scan(/\\/)
|
100
|
+
value << scanner.getch unless scanner.eos?
|
101
|
+
end
|
102
|
+
value << scanner.scan(/[^\"\\]*/)
|
103
|
+
end
|
104
|
+
else
|
105
|
+
value << scanner.scan(/[^\s;\"]*/)
|
106
|
+
end
|
107
|
+
end
|
108
|
+
@parameters[key.downcase.freeze] = value.freeze
|
109
|
+
end
|
110
|
+
end
|
111
|
+
|
112
|
+
@parameters.freeze
|
113
|
+
|
114
|
+
freeze
|
115
|
+
end
|
116
|
+
|
117
|
+
# @return [String, nil] the media type of this content type.
|
118
|
+
# e.g. "application/vnd.github+json" in content-type: application/vnd.github+json; charset="utf-8"
|
119
|
+
attr_reader :media_type
|
120
|
+
|
121
|
+
# @return [String, nil] the 'type' portion of our media type.
|
122
|
+
# e.g. "application" in content-type: application/vnd.github+json; charset="utf-8"
|
123
|
+
attr_reader :type
|
124
|
+
|
125
|
+
# @return [String, nil] the 'subtype' portion of our media type.
|
126
|
+
# e.g. "vnd.github+json" in content-type: application/vnd.github+json; charset="utf-8"
|
127
|
+
attr_reader :subtype
|
128
|
+
|
129
|
+
# @return [String, nil] the 'facet' portion of our media type.
|
130
|
+
# e.g. "vnd" in content-type: application/vnd.github+json; charset="utf-8"
|
131
|
+
attr_reader :facet
|
132
|
+
|
133
|
+
# @return [String, nil] the 'suffix' portion of our media type.
|
134
|
+
# e.g. "json" in content-type: application/vnd.github+json; charset="utf-8"
|
135
|
+
attr_reader :suffix
|
136
|
+
|
137
|
+
# @return [Hash<String, String>] parameters of this content type.
|
138
|
+
# e.g. {"charset" => "utf-8"} in content-type: application/vnd.github+json; charset="utf-8"
|
139
|
+
attr_reader :parameters
|
140
|
+
|
141
|
+
# @param other_type
|
142
|
+
# @return [Boolean] is the 'type' portion of our media type equal (case-insensitive) to the given other_type
|
143
|
+
def type?(other_type)
|
144
|
+
type && type.casecmp?(other_type)
|
145
|
+
end
|
146
|
+
|
147
|
+
# @param other_subtype
|
148
|
+
# @return [Boolean] is the 'subtype' portion of our media type equal (case-insensitive) to the given other_subtype
|
149
|
+
def subtype?(other_subtype)
|
150
|
+
subtype && subtype.casecmp?(other_subtype)
|
151
|
+
end
|
152
|
+
|
153
|
+
# @param other_suffix
|
154
|
+
# @return [Boolean] is the 'suffix' portion of our media type equal (case-insensitive) to the given other_suffix
|
155
|
+
def suffix?(other_suffix)
|
156
|
+
suffix && suffix.casecmp?(other_suffix)
|
157
|
+
end
|
158
|
+
|
159
|
+
SOME_TEXT_SUBTYPES = %w(
|
160
|
+
x-www-form-urlencoded
|
161
|
+
json
|
162
|
+
json-seq
|
163
|
+
jwt
|
164
|
+
jose
|
165
|
+
yaml
|
166
|
+
x-yaml
|
167
|
+
xml
|
168
|
+
html
|
169
|
+
css
|
170
|
+
javascript
|
171
|
+
ecmascript
|
172
|
+
).map(&:freeze).freeze
|
173
|
+
|
174
|
+
# @param unknown [Boolean] return this value when we have no idea whether
|
175
|
+
# our media type is binary or text.
|
176
|
+
# @return [Boolean] does this content type appear to be binary?
|
177
|
+
# this library makes its best guess based on a very incomplete knowledge
|
178
|
+
# of which media types indicate binary or text.
|
179
|
+
def binary?(unknown: true)
|
180
|
+
return false if type_text?
|
181
|
+
|
182
|
+
SOME_TEXT_SUBTYPES.each do |cmpsubtype|
|
183
|
+
return false if (suffix ? suffix.casecmp?(cmpsubtype) : subtype ? subtype.casecmp?(cmpsubtype) : false)
|
184
|
+
end
|
185
|
+
|
186
|
+
# these are generally binary
|
187
|
+
return true if type_image? || type_audio? || type_video?
|
188
|
+
|
189
|
+
# we're out of ideas
|
190
|
+
return unknown
|
191
|
+
end
|
192
|
+
|
193
|
+
# @return [Boolean] is this a JSON content type?
|
194
|
+
def json?
|
195
|
+
suffix ? suffix.casecmp?('json') : subtype ? subtype.casecmp?('json') : false
|
196
|
+
end
|
197
|
+
|
198
|
+
# @return [Boolean] is this an XML content type?
|
199
|
+
def xml?
|
200
|
+
suffix ? suffix.casecmp?('xml'): subtype ? subtype.casecmp?('xml') : false
|
201
|
+
end
|
202
|
+
|
203
|
+
# @return [Boolean] is this a x-www-form-urlencoded content type?
|
204
|
+
def form_urlencoded?
|
205
|
+
suffix ? suffix.casecmp?('x-www-form-urlencoded'): subtype ? subtype.casecmp?('x-www-form-urlencoded') : false
|
206
|
+
end
|
207
|
+
|
208
|
+
# @return [Boolean] is the 'type' portion of our media type 'text'
|
209
|
+
def type_text?
|
210
|
+
type && type.casecmp?('text')
|
211
|
+
end
|
212
|
+
|
213
|
+
# @return [Boolean] is the 'type' portion of our media type 'image'
|
214
|
+
def type_image?
|
215
|
+
type && type.casecmp?('image')
|
216
|
+
end
|
217
|
+
|
218
|
+
# @return [Boolean] is the 'type' portion of our media type 'audio'
|
219
|
+
def type_audio?
|
220
|
+
type && type.casecmp?('audio')
|
221
|
+
end
|
222
|
+
|
223
|
+
# @return [Boolean] is the 'type' portion of our media type 'video'
|
224
|
+
def type_video?
|
225
|
+
type && type.casecmp?('video')
|
226
|
+
end
|
227
|
+
|
228
|
+
# @return [Boolean] is the 'type' portion of our media type 'application'
|
229
|
+
def type_application?
|
230
|
+
type && type.casecmp?('application')
|
231
|
+
end
|
232
|
+
|
233
|
+
# @return [Boolean] is the 'type' portion of our media type 'message'
|
234
|
+
def type_message?
|
235
|
+
type && type.casecmp?('message')
|
236
|
+
end
|
237
|
+
|
238
|
+
# @return [Boolean] is the 'type' portion of our media type 'multipart'
|
239
|
+
def type_multipart?
|
240
|
+
type && type.casecmp?('multipart')
|
241
|
+
end
|
242
|
+
end
|
243
|
+
end
|
@@ -20,20 +20,30 @@ class Ur
|
|
20
20
|
end
|
21
21
|
include FaradayEntity
|
22
22
|
|
23
|
-
def content_type_attrs
|
24
|
-
return @content_type_attrs if instance_variable_defined?(:@content_type_attrs)
|
25
|
-
@content_type_attrs = ContentTypeAttrs.new(content_type)
|
26
|
-
end
|
27
|
-
|
28
23
|
def content_type
|
29
24
|
headers.each do |k, v|
|
30
|
-
return v if k =~ /\Acontent[-_]type\z/i
|
25
|
+
return ContentType.new(v) if k =~ /\Acontent[-_]type\z/i
|
31
26
|
end
|
32
27
|
nil
|
33
28
|
end
|
34
29
|
|
35
30
|
def media_type
|
36
|
-
|
31
|
+
content_type ? content_type.media_type : nil
|
32
|
+
end
|
33
|
+
|
34
|
+
# @return [Boolean] is our content type JSON?
|
35
|
+
def json?
|
36
|
+
content_type && content_type.json?
|
37
|
+
end
|
38
|
+
|
39
|
+
# @return [Boolean] is our content type XML?
|
40
|
+
def xml?
|
41
|
+
content_type && content_type.json?
|
42
|
+
end
|
43
|
+
|
44
|
+
# @return [Boolean] is our content type x-www-form-urlencoded?
|
45
|
+
def form_urlencoded?
|
46
|
+
content_type && content_type.form_urlencoded?
|
37
47
|
end
|
38
48
|
end
|
39
49
|
end
|
data/lib/ur/version.rb
CHANGED
@@ -1 +1 @@
|
|
1
|
-
UR_VERSION = "0.0
|
1
|
+
UR_VERSION = "0.1.0".freeze
|
@@ -0,0 +1,45 @@
|
|
1
|
+
id: https://schemas.ur.unth.net/ur
|
2
|
+
type: object
|
3
|
+
properties:
|
4
|
+
bound:
|
5
|
+
type: string
|
6
|
+
description: '[rfc2616] Inbound and outbound refer to the request and response paths for messages: "inbound" means "traveling toward the origin server", and "outbound" means "traveling toward the user agent"'
|
7
|
+
enum:
|
8
|
+
- inbound
|
9
|
+
- outbound
|
10
|
+
request:
|
11
|
+
type: object
|
12
|
+
properties:
|
13
|
+
method:
|
14
|
+
type: string
|
15
|
+
description: '[rfc2616] The Method token indicates the method to be performed on the resource identified by the Request-URI.'
|
16
|
+
example: POST
|
17
|
+
uri:
|
18
|
+
type: string
|
19
|
+
format: uri
|
20
|
+
example: https://example.com/foo?bar=baz
|
21
|
+
headers:
|
22
|
+
type: object
|
23
|
+
body:
|
24
|
+
type: string
|
25
|
+
response:
|
26
|
+
type: object
|
27
|
+
properties:
|
28
|
+
status:
|
29
|
+
type: integer
|
30
|
+
example: 200
|
31
|
+
headers:
|
32
|
+
type: object
|
33
|
+
body:
|
34
|
+
type: string
|
35
|
+
metadata:
|
36
|
+
type: object
|
37
|
+
properties:
|
38
|
+
began_at_s:
|
39
|
+
type: string
|
40
|
+
duration:
|
41
|
+
type: number
|
42
|
+
tags:
|
43
|
+
type: array
|
44
|
+
items:
|
45
|
+
type: string
|
@@ -0,0 +1,342 @@
|
|
1
|
+
require_relative 'test_helper'
|
2
|
+
|
3
|
+
describe 'Ur::ContentType' do
|
4
|
+
let(:content_type) { Ur::ContentType.new(content_type_str) }
|
5
|
+
describe 'application/vnd.github+json; charset="utf-8"' do
|
6
|
+
let(:content_type_str) { 'application/vnd.github+json; charset="utf-8"' }
|
7
|
+
it 'parses' do
|
8
|
+
assert_equal(content_type, content_type_str)
|
9
|
+
|
10
|
+
assert_equal('application/vnd.github+json', content_type.media_type)
|
11
|
+
|
12
|
+
assert_equal('application', content_type.type)
|
13
|
+
assert(content_type.type?('Application'))
|
14
|
+
assert(content_type.type_application?)
|
15
|
+
|
16
|
+
assert_equal('vnd.github+json', content_type.subtype)
|
17
|
+
assert_equal('vnd', content_type.facet)
|
18
|
+
assert_equal('json', content_type.suffix)
|
19
|
+
|
20
|
+
assert_equal({'charset' => 'utf-8'}, content_type.parameters)
|
21
|
+
assert_equal('utf-8', content_type.parameters['CharSet'])
|
22
|
+
end
|
23
|
+
end
|
24
|
+
describe 'no subtype' do
|
25
|
+
let(:content_type_str) { 'application; charset="utf-8"' }
|
26
|
+
it 'will allow it' do
|
27
|
+
assert_equal(content_type, content_type_str)
|
28
|
+
|
29
|
+
assert_equal('application', content_type.media_type)
|
30
|
+
|
31
|
+
assert_equal('application', content_type.type)
|
32
|
+
assert(content_type.type?('Application'))
|
33
|
+
|
34
|
+
assert_equal(nil, content_type.subtype)
|
35
|
+
assert_equal(nil, content_type.facet)
|
36
|
+
assert_equal(nil, content_type.suffix)
|
37
|
+
|
38
|
+
assert_equal({'charset' => 'utf-8'}, content_type.parameters)
|
39
|
+
assert_equal('utf-8', content_type.parameters['CharSet'])
|
40
|
+
end
|
41
|
+
end
|
42
|
+
describe 'no facet' do
|
43
|
+
let(:content_type_str) { 'application/github+json; charset="utf-8"' }
|
44
|
+
it 'parses' do
|
45
|
+
assert_equal(content_type, content_type_str)
|
46
|
+
|
47
|
+
assert_equal('application/github+json', content_type.media_type)
|
48
|
+
|
49
|
+
assert_equal('application', content_type.type)
|
50
|
+
assert(content_type.type?('Application'))
|
51
|
+
|
52
|
+
assert_equal('github+json', content_type.subtype)
|
53
|
+
assert_equal(nil, content_type.facet)
|
54
|
+
assert_equal('json', content_type.suffix)
|
55
|
+
|
56
|
+
assert_equal({'charset' => 'utf-8'}, content_type.parameters)
|
57
|
+
assert_equal('utf-8', content_type.parameters['CharSet'])
|
58
|
+
end
|
59
|
+
end
|
60
|
+
describe 'no suffix' do
|
61
|
+
let(:content_type_str) { 'application/vnd.github.json; charset="utf-8"' }
|
62
|
+
it 'parses' do
|
63
|
+
assert_equal(content_type, content_type_str)
|
64
|
+
|
65
|
+
assert_equal('application/vnd.github.json', content_type.media_type)
|
66
|
+
|
67
|
+
assert_equal('application', content_type.type)
|
68
|
+
assert(content_type.type?('Application'))
|
69
|
+
|
70
|
+
assert_equal('vnd.github.json', content_type.subtype)
|
71
|
+
assert_equal('vnd', content_type.facet)
|
72
|
+
assert_equal(nil, content_type.suffix)
|
73
|
+
|
74
|
+
assert_equal({'charset' => 'utf-8'}, content_type.parameters)
|
75
|
+
assert_equal('utf-8', content_type.parameters['CharSet'])
|
76
|
+
end
|
77
|
+
describe('[invalid] quote in type') do
|
78
|
+
let(:content_type_str) { 'applic"ation/foo; foo=bar' }
|
79
|
+
it('gives up') do
|
80
|
+
assert_equal('applic', content_type.type)
|
81
|
+
assert_equal(nil, content_type.subtype)
|
82
|
+
end
|
83
|
+
end
|
84
|
+
describe('[invalid] backslash in type') do
|
85
|
+
let(:content_type_str) { 'applicati\on/foo; foo=bar' }
|
86
|
+
it('parses') do
|
87
|
+
assert_equal('applicati\\on', content_type.type)
|
88
|
+
assert_equal('foo', content_type.subtype)
|
89
|
+
end
|
90
|
+
end
|
91
|
+
describe('[invalid] quote in subtype') do
|
92
|
+
let(:content_type_str) { 'application/f"oo; foo=bar' }
|
93
|
+
it('gives up') do
|
94
|
+
assert_equal('application', content_type.type)
|
95
|
+
assert_equal('f', content_type.subtype)
|
96
|
+
end
|
97
|
+
end
|
98
|
+
describe('[invalid] backslash in subtype') do
|
99
|
+
let(:content_type_str) { 'application/fo\\o; foo=bar' }
|
100
|
+
it('parses') do
|
101
|
+
assert_equal('application', content_type.type)
|
102
|
+
assert_equal('fo\\o', content_type.subtype)
|
103
|
+
end
|
104
|
+
end
|
105
|
+
end
|
106
|
+
describe 'parameters' do
|
107
|
+
describe 'basic usage' do
|
108
|
+
let(:content_type_str) { 'application/foo; charset="utf-8"; foo=bar' }
|
109
|
+
it('parses') do
|
110
|
+
assert_equal({'charset' => 'utf-8', 'foo' => 'bar'}, content_type.parameters)
|
111
|
+
end
|
112
|
+
end
|
113
|
+
describe 'params with capitalization' do
|
114
|
+
let(:content_type_str) { 'application/foo; Charset="utf-8"; FOO=bar' }
|
115
|
+
it('parses') do
|
116
|
+
assert_equal({'charset' => 'utf-8', 'foo' => 'bar'}, content_type.parameters)
|
117
|
+
assert_equal('utf-8', content_type.parameters['CharSet'])
|
118
|
+
assert_equal('utf-8', content_type.parameters['Charset'])
|
119
|
+
assert_equal('bar', content_type.parameters['foo'])
|
120
|
+
assert_equal('bar', content_type.parameters['FOO'])
|
121
|
+
end
|
122
|
+
end
|
123
|
+
describe 'repeated params' do
|
124
|
+
let(:content_type_str) { 'application/foo; foo="first"; foo=second' }
|
125
|
+
it('will just overwrite') do
|
126
|
+
assert_equal({'foo' => 'second'}, content_type.parameters)
|
127
|
+
end
|
128
|
+
end
|
129
|
+
describe 'repeated params, different capitalization' do
|
130
|
+
let(:content_type_str) { 'application/foo; FOO=first; Foo=second' }
|
131
|
+
it('will just overwrite') do
|
132
|
+
assert_equal({'foo' => 'second'}, content_type.parameters)
|
133
|
+
end
|
134
|
+
end
|
135
|
+
describe 'empty strings' do
|
136
|
+
let(:content_type_str) { 'application/foo; empty1=; empty2=""' }
|
137
|
+
it('parses') do
|
138
|
+
assert_equal({'empty1' => '', 'empty2' => ''}, content_type.parameters)
|
139
|
+
end
|
140
|
+
end
|
141
|
+
describe 'empty strings with whitespace' do
|
142
|
+
let(:content_type_str) { 'application/foo; empty1= ; empty2="" ' }
|
143
|
+
it('parses') do
|
144
|
+
assert_equal({'empty1' => '', 'empty2' => ''}, content_type.parameters)
|
145
|
+
end
|
146
|
+
end
|
147
|
+
describe('[invalid] opening quote only') do
|
148
|
+
let(:content_type_str) { 'application/foo; foo=1; bar="' }
|
149
|
+
it('parses') do
|
150
|
+
assert_equal({'foo' => '1', 'bar' => ''}, content_type.parameters)
|
151
|
+
end
|
152
|
+
end
|
153
|
+
describe('[invalid] backlash with no character') do
|
154
|
+
let(:content_type_str) { 'application/foo; foo=1; bar="\\' }
|
155
|
+
it('parses') do
|
156
|
+
assert_equal({'foo' => '1', 'bar' => ''}, content_type.parameters)
|
157
|
+
end
|
158
|
+
end
|
159
|
+
describe('[invalid] extra following quoted string') do
|
160
|
+
let(:content_type_str) { 'application/foo; foo="1" 2; bar=3' }
|
161
|
+
it('sorta parses') do
|
162
|
+
assert_equal({'foo' => '1 2', 'bar' => '3'}, content_type.parameters)
|
163
|
+
end
|
164
|
+
end
|
165
|
+
describe('[invalid] quotes silliness') do
|
166
|
+
let(:content_type_str) { 'application/foo; foo="1" 2 "3 4" "5 " ; bar=3' }
|
167
|
+
it('sorta parses') do
|
168
|
+
assert_equal({'foo' => '1 2 3 4 5 ', 'bar' => '3'}, content_type.parameters)
|
169
|
+
end
|
170
|
+
end
|
171
|
+
describe('[invalid] backlash quote') do
|
172
|
+
let(:content_type_str) { 'application/foo; foo=1; bar="\\"' }
|
173
|
+
it('parses') do
|
174
|
+
assert_equal({'foo' => '1', 'bar' => '"'}, content_type.parameters)
|
175
|
+
end
|
176
|
+
end
|
177
|
+
describe('[invalid] trailing ;') do
|
178
|
+
let(:content_type_str) { 'application/foo; foo=bar;' }
|
179
|
+
it('parses') do
|
180
|
+
assert_equal({'foo' => 'bar'}, content_type.parameters)
|
181
|
+
end
|
182
|
+
end
|
183
|
+
describe('[invalid] extra ; inline') do
|
184
|
+
let(:content_type_str) { 'application/foo; ; ; foo=bar' }
|
185
|
+
it('parses') do
|
186
|
+
assert_equal({'foo' => 'bar'}, content_type.parameters)
|
187
|
+
end
|
188
|
+
end
|
189
|
+
describe('[invalid] whitespace around the =') do
|
190
|
+
let(:content_type_str) { 'application/foo; foo = bar; baz = qux' }
|
191
|
+
it('parses') do
|
192
|
+
assert_equal({'foo ' => ' bar', 'baz ' => ' qux'}, content_type.parameters)
|
193
|
+
end
|
194
|
+
end
|
195
|
+
describe('whitespace before the ;') do
|
196
|
+
let(:content_type_str) { 'application/foo; foo=bar ; baz=qux' }
|
197
|
+
it('parses') do
|
198
|
+
assert_equal({'foo' => 'bar', 'baz' => 'qux'}, content_type.parameters)
|
199
|
+
end
|
200
|
+
end
|
201
|
+
describe('no media_type') do
|
202
|
+
let(:content_type_str) { '; foo=bar' }
|
203
|
+
it('parses') do
|
204
|
+
assert_equal({'foo' => 'bar'}, content_type.parameters)
|
205
|
+
end
|
206
|
+
end
|
207
|
+
describe('[invalid] quote in parameter name') do
|
208
|
+
let(:content_type_str) { 'application/foo; fo"o=bar' }
|
209
|
+
it('gives up') do
|
210
|
+
assert_equal({}, content_type.parameters)
|
211
|
+
end
|
212
|
+
end
|
213
|
+
describe('[invalid] backslash in parameter name') do
|
214
|
+
let(:content_type_str) { 'application/foo; fo\\o=bar' }
|
215
|
+
it('parses') do
|
216
|
+
assert_equal({'fo\\o' => 'bar'}, content_type.parameters)
|
217
|
+
end
|
218
|
+
end
|
219
|
+
end
|
220
|
+
describe 'binary?' do
|
221
|
+
binary_content_type_strs = [
|
222
|
+
'audio/ogg',
|
223
|
+
'Audio/OGG; foo=bar',
|
224
|
+
'VIDEO/foo+bar',
|
225
|
+
]
|
226
|
+
binary_content_type_strs.each do |binary_content_type_str|
|
227
|
+
describe(binary_content_type_str) do
|
228
|
+
let(:content_type_str) { binary_content_type_str }
|
229
|
+
it 'is binary' do
|
230
|
+
assert(content_type.binary?(unknown: false))
|
231
|
+
end
|
232
|
+
end
|
233
|
+
end
|
234
|
+
not_binary_content_type_strs = [
|
235
|
+
'text/anything',
|
236
|
+
'TEXT/plain; charset=foo',
|
237
|
+
'application/JSON',
|
238
|
+
'media/foo+Json; foo="bar"',
|
239
|
+
]
|
240
|
+
not_binary_content_type_strs.each do |not_binary_content_type_str|
|
241
|
+
describe(not_binary_content_type_str) do
|
242
|
+
let(:content_type_str) { not_binary_content_type_str }
|
243
|
+
it 'is not binary' do
|
244
|
+
assert(!content_type.binary?(unknown: true))
|
245
|
+
end
|
246
|
+
end
|
247
|
+
end
|
248
|
+
unknown_content_type_strs = [
|
249
|
+
'foo',
|
250
|
+
'foo/bar; note="not application/json"',
|
251
|
+
'application/jsonisnotthis',
|
252
|
+
'application/octet-stream',
|
253
|
+
]
|
254
|
+
unknown_content_type_strs.each do |unknown_content_type_str|
|
255
|
+
describe(unknown_content_type_str) do
|
256
|
+
let(:content_type_str) { unknown_content_type_str }
|
257
|
+
it 'is unknown' do
|
258
|
+
assert(content_type.binary?(unknown: true))
|
259
|
+
assert(!content_type.binary?(unknown: false))
|
260
|
+
end
|
261
|
+
end
|
262
|
+
end
|
263
|
+
end
|
264
|
+
describe 'json?' do
|
265
|
+
json_content_type_strs = [
|
266
|
+
'application/json',
|
267
|
+
'Application/Json; charset=EBCDIC',
|
268
|
+
'Text/json',
|
269
|
+
'MEDIA/JSON',
|
270
|
+
'media/foo.bar+json',
|
271
|
+
'media/foo.bar+json; foo=',
|
272
|
+
]
|
273
|
+
json_content_type_strs.each do |json_content_type_str|
|
274
|
+
describe(json_content_type_str) do
|
275
|
+
let(:content_type_str) { json_content_type_str }
|
276
|
+
it 'is json' do
|
277
|
+
assert(content_type.json?)
|
278
|
+
end
|
279
|
+
end
|
280
|
+
end
|
281
|
+
not_json_content_type_strs = [
|
282
|
+
'json',
|
283
|
+
'foo/bar; note="not application/json"',
|
284
|
+
'application/jsonisnotthis',
|
285
|
+
'text/json+xml', # I don't even know what I'm trying for here
|
286
|
+
]
|
287
|
+
not_json_content_type_strs.each do |not_json_content_type_str|
|
288
|
+
describe(not_json_content_type_str) do
|
289
|
+
let(:content_type_str) { not_json_content_type_str }
|
290
|
+
it 'is not json' do
|
291
|
+
assert(!content_type.json?)
|
292
|
+
end
|
293
|
+
end
|
294
|
+
end
|
295
|
+
end
|
296
|
+
describe 'xml?' do
|
297
|
+
xml_content_type_strs = [
|
298
|
+
'application/xml',
|
299
|
+
'Application/Xml; charset=EBCDIC',
|
300
|
+
'Text/xml',
|
301
|
+
'MEDIA/XML',
|
302
|
+
'media/foo.bar+xml',
|
303
|
+
'media/foo.bar+xml; foo=',
|
304
|
+
]
|
305
|
+
xml_content_type_strs.each do |xml_content_type_str|
|
306
|
+
describe(xml_content_type_str) do
|
307
|
+
let(:content_type_str) { xml_content_type_str }
|
308
|
+
it 'is xml' do
|
309
|
+
assert(content_type.xml?)
|
310
|
+
end
|
311
|
+
end
|
312
|
+
end
|
313
|
+
not_xml_content_type_strs = [
|
314
|
+
'xml',
|
315
|
+
'foo/bar; note="not application/xml"',
|
316
|
+
'application/xmlisnotthis',
|
317
|
+
'text/xml+json', # I don't even know what I'm trying for here
|
318
|
+
]
|
319
|
+
not_xml_content_type_strs.each do |not_xml_content_type_str|
|
320
|
+
describe(not_xml_content_type_str) do
|
321
|
+
let(:content_type_str) { not_xml_content_type_str }
|
322
|
+
it 'is not xml' do
|
323
|
+
assert(!content_type.xml?)
|
324
|
+
end
|
325
|
+
end
|
326
|
+
end
|
327
|
+
end
|
328
|
+
describe 'form_urlencoded?' do
|
329
|
+
describe('application/x-www-form-urlencoded') do
|
330
|
+
let(:content_type_str) { 'application/x-www-form-urlencoded' }
|
331
|
+
it 'is form_urlencoded' do
|
332
|
+
assert(content_type.form_urlencoded?)
|
333
|
+
end
|
334
|
+
end
|
335
|
+
describe('application/foo') do
|
336
|
+
let(:content_type_str) { 'application/foo' }
|
337
|
+
it 'is not form_urlencoded' do
|
338
|
+
assert(!content_type.form_urlencoded?)
|
339
|
+
end
|
340
|
+
end
|
341
|
+
end
|
342
|
+
end
|
data/test/test_helper.rb
CHANGED
@@ -11,9 +11,20 @@ require 'minitest/reporters'
|
|
11
11
|
Minitest::Reporters.use! Minitest::Reporters::SpecReporter.new
|
12
12
|
|
13
13
|
class UrSpec < Minitest::Spec
|
14
|
+
if ENV['UR_TEST_ALPHA']
|
15
|
+
# :nocov:
|
16
|
+
define_singleton_method(:test_order) { :alpha }
|
17
|
+
# :nocov:
|
18
|
+
end
|
19
|
+
|
14
20
|
def assert_json_equal(exp, act, *a)
|
15
21
|
assert_equal(JSI::Typelike.as_json(exp), JSI::Typelike.as_json(act), *a)
|
16
22
|
end
|
23
|
+
|
24
|
+
def assert_equal exp, act, msg = nil
|
25
|
+
msg = message(msg, E) { diff exp, act }
|
26
|
+
assert exp == act, msg
|
27
|
+
end
|
17
28
|
end
|
18
29
|
|
19
30
|
# register this to be the base class for specs instead of Minitest::Spec
|
@@ -0,0 +1,11 @@
|
|
1
|
+
require_relative 'test_helper'
|
2
|
+
|
3
|
+
describe 'Ur metadata' do
|
4
|
+
it 'sets duration from began_at' do
|
5
|
+
ur = Ur.new
|
6
|
+
ur.metadata.began_at = Time.now
|
7
|
+
ur.metadata.finish!
|
8
|
+
assert_instance_of(Float, ur.metadata.duration)
|
9
|
+
assert_operator(ur.metadata.duration, :>, 0)
|
10
|
+
end
|
11
|
+
end
|
data/test/ur_rack_test.rb
CHANGED
@@ -10,8 +10,8 @@ describe 'Ur rack integration' do
|
|
10
10
|
assert_equal('bar', ur.request.headers['foo'])
|
11
11
|
assert_equal('https://ur.unth.net/', ur.request.uri)
|
12
12
|
assert(ur.response.empty?)
|
13
|
-
assert_instance_of(Time, ur.
|
14
|
-
assert_nil(ur.
|
13
|
+
assert_instance_of(Time, ur.metadata.began_at)
|
14
|
+
assert_nil(ur.metadata.duration)
|
15
15
|
assert(ur.validate)
|
16
16
|
end
|
17
17
|
it 'builds from a rack request' do
|
@@ -22,8 +22,8 @@ describe 'Ur rack integration' do
|
|
22
22
|
assert_equal('bar', ur.request.headers['foo'])
|
23
23
|
assert_equal('https://ur.unth.net/', ur.request.uri)
|
24
24
|
assert(ur.response.empty?)
|
25
|
-
assert_instance_of(Time, ur.
|
26
|
-
assert_nil(ur.
|
25
|
+
assert_instance_of(Time, ur.metadata.began_at)
|
26
|
+
assert_nil(ur.metadata.duration)
|
27
27
|
assert(ur.validate)
|
28
28
|
end
|
29
29
|
end
|
data/test/ur_test.rb
CHANGED
@@ -37,8 +37,8 @@ describe 'Ur' do
|
|
37
37
|
assert_equal('bar', ur.request.headers['foo'])
|
38
38
|
assert_equal('https://ur.unth.net/', ur.request.uri)
|
39
39
|
assert(ur.response.empty?)
|
40
|
-
assert_instance_of(Time, ur.
|
41
|
-
assert_nil(ur.
|
40
|
+
assert_instance_of(Time, ur.metadata.began_at)
|
41
|
+
assert_nil(ur.metadata.duration)
|
42
42
|
assert(ur.validate)
|
43
43
|
end,
|
44
44
|
after_response: -> (ur) do
|
@@ -51,10 +51,10 @@ describe 'Ur' do
|
|
51
51
|
assert_equal(200, ur.response.status)
|
52
52
|
assert_equal('text/plain', ur.response.headers['Content-Type'])
|
53
53
|
assert_equal('ᚒ', ur.response.body)
|
54
|
-
assert_instance_of(Time, ur.
|
55
|
-
assert_instance_of(Float, ur.
|
56
|
-
assert_operator(ur.
|
57
|
-
assert_equal(['ur_test_rack'], ur.
|
54
|
+
assert_instance_of(Time, ur.metadata.began_at)
|
55
|
+
assert_instance_of(Float, ur.metadata.duration)
|
56
|
+
assert_operator(ur.metadata.duration, :>, 0)
|
57
|
+
assert_equal(['ur_test_rack'], ur.metadata.tags.to_a)
|
58
58
|
assert(ur.validate)
|
59
59
|
end,
|
60
60
|
)
|
@@ -71,8 +71,8 @@ describe 'Ur' do
|
|
71
71
|
assert_equal('https://ur.unth.net/', ur.request.uri)
|
72
72
|
assert_equal(Addressable::URI.parse('https://ur.unth.net/'), ur.request.addressable_uri)
|
73
73
|
assert(ur.response.empty?)
|
74
|
-
assert_instance_of(Time, ur.
|
75
|
-
assert_nil(ur.
|
74
|
+
assert_instance_of(Time, ur.metadata.began_at)
|
75
|
+
assert_nil(ur.metadata.duration)
|
76
76
|
assert(ur.validate)
|
77
77
|
end,
|
78
78
|
after_response: -> (ur) do
|
@@ -85,10 +85,10 @@ describe 'Ur' do
|
|
85
85
|
assert_equal(200, ur.response.status)
|
86
86
|
assert_equal('text/plain', ur.response.headers['Content-Type'])
|
87
87
|
assert_equal('ᚒ', ur.response.body)
|
88
|
-
assert_instance_of(Time, ur.
|
89
|
-
assert_instance_of(Float, ur.
|
90
|
-
assert_operator(ur.
|
91
|
-
assert_equal(['ur_test_faraday'], ur.
|
88
|
+
assert_instance_of(Time, ur.metadata.began_at)
|
89
|
+
assert_instance_of(Float, ur.metadata.duration)
|
90
|
+
assert_operator(ur.metadata.duration, :>, 0)
|
91
|
+
assert_equal(['ur_test_faraday'], ur.metadata.tags.to_a)
|
92
92
|
assert(ur.validate)
|
93
93
|
end,
|
94
94
|
)
|
metadata
CHANGED
@@ -1,14 +1,14 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: ur
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 0.0
|
4
|
+
version: 0.1.0
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Ethan
|
8
8
|
autorequire:
|
9
9
|
bindir: exe
|
10
10
|
cert_chain: []
|
11
|
-
date: 2019-
|
11
|
+
date: 2019-12-21 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
14
|
name: jsi
|
@@ -181,19 +181,21 @@ files:
|
|
181
181
|
- README.md
|
182
182
|
- Rakefile.rb
|
183
183
|
- lib/ur.rb
|
184
|
-
- lib/ur/
|
184
|
+
- lib/ur/content_type.rb
|
185
185
|
- lib/ur/faraday.rb
|
186
186
|
- lib/ur/faraday/yield_ur.rb
|
187
|
+
- lib/ur/metadata.rb
|
187
188
|
- lib/ur/middleware.rb
|
188
|
-
- lib/ur/processing.rb
|
189
189
|
- lib/ur/request.rb
|
190
190
|
- lib/ur/request_and_response.rb
|
191
191
|
- lib/ur/response.rb
|
192
192
|
- lib/ur/sub_ur.rb
|
193
193
|
- lib/ur/version.rb
|
194
|
+
- resources/ur.schema.yml
|
195
|
+
- test/content_type_test.rb
|
194
196
|
- test/test_helper.rb
|
195
197
|
- test/ur_faraday_test.rb
|
196
|
-
- test/
|
198
|
+
- test/ur_metadata_test.rb
|
197
199
|
- test/ur_rack_test.rb
|
198
200
|
- test/ur_test.rb
|
199
201
|
- ur.gemspec
|
@@ -222,9 +224,10 @@ signing_key:
|
|
222
224
|
specification_version: 4
|
223
225
|
summary: 'ur: unified request representation'
|
224
226
|
test_files:
|
227
|
+
- test/content_type_test.rb
|
225
228
|
- test/test_helper.rb
|
226
229
|
- test/ur_faraday_test.rb
|
227
|
-
- test/
|
230
|
+
- test/ur_metadata_test.rb
|
228
231
|
- test/ur_rack_test.rb
|
229
232
|
- test/ur_test.rb
|
230
233
|
- ".simplecov"
|
@@ -1,83 +0,0 @@
|
|
1
|
-
require 'ur' unless Object.const_defined?(:Ur)
|
2
|
-
|
3
|
-
class Ur
|
4
|
-
# parses attributes out of content type header
|
5
|
-
class ContentTypeAttrs
|
6
|
-
def initialize(content_type)
|
7
|
-
raise(ArgumentError) unless content_type.nil? || content_type.respond_to?(:to_str)
|
8
|
-
content_type = content_type.to_str if content_type
|
9
|
-
@media_type = (content_type.split(/\s*[;]\s*/, 2).first if content_type)
|
10
|
-
@media_type.strip! if @media_type
|
11
|
-
@content_type = content_type
|
12
|
-
@parsed = false
|
13
|
-
@attributes = Hash.new { |h,k| h[k] = [] }
|
14
|
-
catch(:unparseable) do
|
15
|
-
throw(:unparseable) unless content_type
|
16
|
-
uri_parser = URI.const_defined?(:Parser) ? URI::Parser.new : URI
|
17
|
-
scanner = StringScanner.new(content_type)
|
18
|
-
scanner.scan(/.*;\s*/) || throw(:unparseable)
|
19
|
-
while scanner.scan(/(\w+)=("?)([^"]*)("?)\s*(,?)\s*/)
|
20
|
-
key = scanner[1]
|
21
|
-
quote1 = scanner[2]
|
22
|
-
value = scanner[3]
|
23
|
-
quote2 = scanner[4]
|
24
|
-
comma_follows = !scanner[5].empty?
|
25
|
-
throw(:unparseable) unless quote1 == quote2
|
26
|
-
throw(:unparseable) if !comma_follows && !scanner.eos?
|
27
|
-
@attributes[uri_parser.unescape(key)] << uri_parser.unescape(value)
|
28
|
-
end
|
29
|
-
throw(:unparseable) unless scanner.eos?
|
30
|
-
@parsed = true
|
31
|
-
end
|
32
|
-
end
|
33
|
-
|
34
|
-
attr_reader :content_type, :media_type
|
35
|
-
|
36
|
-
def parsed?
|
37
|
-
@parsed
|
38
|
-
end
|
39
|
-
|
40
|
-
def [](key)
|
41
|
-
@attributes[key]
|
42
|
-
end
|
43
|
-
|
44
|
-
def text?
|
45
|
-
# ordered hash by priority mapping types to binary or text
|
46
|
-
# regexps will have \A and \z added
|
47
|
-
types = {
|
48
|
-
%r(image/.*) => :binary,
|
49
|
-
%r(audio/.*) => :binary,
|
50
|
-
%r(video/.*) => :binary,
|
51
|
-
%r(model/.*) => :binary,
|
52
|
-
%r(text/.*) => :text,
|
53
|
-
%r(message/.*) => :text,
|
54
|
-
%r(application/(.+\+)?json) => :text,
|
55
|
-
%r(application/(.+\+)?xml) => :text,
|
56
|
-
%r(model/(.+\+)?xml) => :text,
|
57
|
-
'application/x-www-form-urlencoded' => :text,
|
58
|
-
'application/javascript' => :text,
|
59
|
-
'application/ecmascript' => :text,
|
60
|
-
'application/octet-stream' => :binary,
|
61
|
-
'application/ogg' => :binary,
|
62
|
-
'application/pdf' => :binary,
|
63
|
-
'application/postscript' => :binary,
|
64
|
-
'application/zip' => :binary,
|
65
|
-
'application/gzip' => :binary,
|
66
|
-
'application/vnd.apple.pkpass' => :binary,
|
67
|
-
}
|
68
|
-
types.each do |match, type|
|
69
|
-
matched = match.is_a?(Regexp) ? media_type =~ %r(\A#{match.source}\z) : media_type == match
|
70
|
-
if matched
|
71
|
-
return type == :text
|
72
|
-
end
|
73
|
-
end
|
74
|
-
# fallback (unknown or not given) assume that unknown content types are binary but omitted
|
75
|
-
# content-type means text
|
76
|
-
if content_type && content_type =~ /\S/
|
77
|
-
return false
|
78
|
-
else
|
79
|
-
return true
|
80
|
-
end
|
81
|
-
end
|
82
|
-
end
|
83
|
-
end
|
data/test/ur_processing_test.rb
DELETED
@@ -1,11 +0,0 @@
|
|
1
|
-
require_relative 'test_helper'
|
2
|
-
|
3
|
-
describe 'Ur processing' do
|
4
|
-
it 'sets duration from began_at' do
|
5
|
-
ur = Ur.new
|
6
|
-
ur.processing.began_at = Time.now
|
7
|
-
ur.processing.finish!
|
8
|
-
assert_instance_of(Float, ur.processing.duration)
|
9
|
-
assert_operator(ur.processing.duration, :>, 0)
|
10
|
-
end
|
11
|
-
end
|