ur 0.0.4 → 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 06e6df921036d0111ac6b57e937508d5689d30bbb702ebfc691749a9c31884ed
4
- data.tar.gz: d160635a422395081f510059b7916aa9264c3289c3cc1d9170769c32c6d1cbb3
3
+ metadata.gz: 551019c356f6c251fc9330e21d58be49983302fa74754598d44a5ce02c8b6fec
4
+ data.tar.gz: 06e531326d20440cf145328ae17f07251053aeb5a4af3ecfbf8d925620b7e89a
5
5
  SHA512:
6
- metadata.gz: 41b9e773174eb1beaa88b128ebde4e75830e4c9bc9ee907bcbf24e9be7157ba0cff8a980bdc8b18a978a02d990d7933dcd3ae88fdacba81ed59f87046d36a559
7
- data.tar.gz: 42dc377e5da9acccb960e35dea7d7d874c5a90b5965b025b0dd2a8c6443b89eae282f1f034bcee450dd8962bb06470d7016cbeaebf24871e3851853db83654ff
6
+ metadata.gz: d68634fbde7ef1511914392857f6248db0a8bba6f18a044b262da2d10912c5059d4eb08c3953d1cc18d2a4fbd8b27bc6face500211b2fbb8c1eef85083aecd84
7
+ data.tar.gz: dbf33cc387daf9d82181330f029335a89c179582d614c2d77d07359c806203aebaa2e2c86fe785f568529f0cb2cdb255a74f37e623404f7335fd5da40f4cb175
@@ -1,3 +1,7 @@
1
+ # v0.1.0
2
+ - rename processing to metadata
3
+ - Ur::ContentType
4
+
1
5
  # v0.0.4
2
6
  - bump JSI v0.2.0
3
7
 
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 processing information.
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 processing information consists of the time the request began, the duration of the request, or tag strings. This is optional.
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
- Ur = JSI.class_for_schema({
8
- id: 'https://schemas.ur.unth.net/ur',
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
- Processing = JSI.class_for_schema(self.schema['properties']['processing'])
22
+ Metadata = JSI.class_for_schema(self.schema['properties']['metadata'])
56
23
  require 'ur/request'
57
24
  require 'ur/response'
58
- require 'ur/processing'
25
+ require 'ur/metadata'
59
26
 
60
- autoload :ContentTypeAttrs, 'ur/content_type_attrs'
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.processing.begin!
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.processing.begin!
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.processing = {} if self.processing.nil?
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
- processing.tags = logger.formatter.current_tags.dup
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
- processing.finish!
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
- processing.finish!
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
@@ -1,7 +1,7 @@
1
1
  require 'ur' unless Object.const_defined?(:Ur)
2
2
 
3
3
  class Ur
4
- class Processing
4
+ class Metadata
5
5
  include SubUr
6
6
 
7
7
  def began_at
@@ -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
- content_type_attrs.media_type
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
@@ -1 +1 @@
1
- UR_VERSION = "0.0.4".freeze
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
@@ -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
@@ -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.processing.began_at)
14
- assert_nil(ur.processing.duration)
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.processing.began_at)
26
- assert_nil(ur.processing.duration)
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
@@ -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.processing.began_at)
41
- assert_nil(ur.processing.duration)
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.processing.began_at)
55
- assert_instance_of(Float, ur.processing.duration)
56
- assert_operator(ur.processing.duration, :>, 0)
57
- assert_equal(['ur_test_rack'], ur.processing.tags.to_a)
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.processing.began_at)
75
- assert_nil(ur.processing.duration)
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.processing.began_at)
89
- assert_instance_of(Float, ur.processing.duration)
90
- assert_operator(ur.processing.duration, :>, 0)
91
- assert_equal(['ur_test_faraday'], ur.processing.tags.to_a)
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
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-27 00:00:00.000000000 Z
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/content_type_attrs.rb
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/ur_processing_test.rb
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/ur_processing_test.rb
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
@@ -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