safrano 0.6.7 → 0.7.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/lib/core_ext/DateTime/format.rb +1 -1
- data/lib/core_ext/Time/format.rb +2 -2
- data/lib/odata/batch.rb +39 -33
- data/lib/odata/collection.rb +2 -3
- data/lib/odata/collection_media.rb +92 -81
- data/lib/odata/common_logger.rb +19 -2
- data/lib/odata/complex_type.rb +5 -9
- data/lib/odata/edm/primitive_types.rb +53 -64
- data/lib/odata/entity.rb +1 -2
- data/lib/odata/error.rb +3 -3
- data/lib/odata/expand.rb +0 -3
- data/lib/odata/function_import.rb +5 -3
- data/lib/odata/model_ext.rb +21 -26
- data/lib/odata/walker.rb +8 -15
- data/lib/safrano/multipart.rb +39 -28
- data/lib/safrano/rack_app.rb +14 -123
- data/lib/safrano/request.rb +127 -8
- data/lib/safrano/service.rb +3 -3
- data/lib/safrano/version.rb +1 -1
- data/lib/sequel/plugins/join_by_paths.rb +48 -7
- metadata +18 -6
data/lib/safrano/rack_app.rb
CHANGED
@@ -6,143 +6,33 @@ require_relative 'request'
|
|
6
6
|
require_relative 'response'
|
7
7
|
|
8
8
|
module Safrano
|
9
|
-
#
|
10
|
-
|
11
|
-
|
12
|
-
|
13
|
-
.if_valid do |_context|
|
14
|
-
# cf. stackoverflow.com/questions/22924678/sinatra-delete-response-headers
|
15
|
-
headers.delete('Content-Type')
|
16
|
-
@response.headers.delete('Content-Type')
|
17
|
-
@response.headers['Content-Type'] = ''
|
18
|
-
[200, EMPTY_HASH, '']
|
19
|
-
end
|
20
|
-
end
|
21
|
-
|
22
|
-
def odata_delete
|
23
|
-
@walker.finalize.tap_error { |err| return err.odata_get(@request) }
|
24
|
-
.if_valid { |context| context.odata_delete(@request) }
|
25
|
-
end
|
26
|
-
|
27
|
-
def odata_put
|
28
|
-
@walker.finalize.tap_error { |err| return err.odata_get(@request) }
|
29
|
-
.if_valid { |context| context.odata_put(@request) }
|
30
|
-
end
|
31
|
-
|
32
|
-
def odata_patch
|
33
|
-
@walker.finalize.tap_error { |err| return err.odata_get(@request) }
|
34
|
-
.if_valid { |context| context.odata_patch(@request) }
|
35
|
-
end
|
36
|
-
|
37
|
-
def odata_get
|
38
|
-
@walker.finalize.tap_error { |err| return err.odata_get(@request) }
|
39
|
-
.if_valid { |context| context.odata_get(@request) }
|
40
|
-
end
|
41
|
-
|
42
|
-
def odata_post
|
43
|
-
@walker.finalize.tap_error { |err| return err.odata_get(@request) }
|
44
|
-
.if_valid { |context| context.odata_post(@request) }
|
45
|
-
end
|
46
|
-
|
47
|
-
def odata_head
|
48
|
-
[200, EMPTY_HASH, [EMPTY_STRING]]
|
49
|
-
end
|
50
|
-
end
|
51
|
-
|
52
|
-
# the main Rack server app. Source: the Rack docu/examples and partly
|
53
|
-
# inspired from Sinatra
|
9
|
+
# Note there is a strong 1 to 1 relation between an app instance
|
10
|
+
# and a published service. --> actually means also
|
11
|
+
# we only support on service per App-class because publishing is
|
12
|
+
# made on app class level
|
54
13
|
class ServerApp
|
55
|
-
|
56
|
-
|
57
|
-
|
58
|
-
|
59
|
-
|
60
|
-
include MethodHandlers
|
61
|
-
|
62
|
-
def before
|
63
|
-
@request.service_base = self.class.get_service_base
|
64
|
-
|
65
|
-
@request.negotiate_service_version.tap_valid do
|
66
|
-
myhdrs = NOCACHE_HDRS.dup
|
67
|
-
myhdrs[DATASERVICEVERSION] = @request.service.data_service_version
|
68
|
-
headers myhdrs
|
69
|
-
end
|
70
|
-
end
|
71
|
-
|
72
|
-
# dispatch for all methods requiring parsing of the path
|
73
|
-
# with walker (ie. allmost all excepted HEAD)
|
74
|
-
def dispatch_with_walker
|
75
|
-
@walker = @request.create_odata_walker
|
76
|
-
case @request.request_method
|
77
|
-
when 'GET'
|
78
|
-
odata_get
|
79
|
-
when 'POST'
|
80
|
-
odata_post
|
81
|
-
when 'DELETE'
|
82
|
-
odata_delete
|
83
|
-
when 'OPTIONS'
|
84
|
-
odata_options
|
85
|
-
when 'PUT'
|
86
|
-
odata_put
|
87
|
-
when 'PATCH', 'MERGE'
|
88
|
-
odata_patch
|
89
|
-
else
|
90
|
-
raise Error
|
91
|
-
end
|
92
|
-
end
|
93
|
-
|
94
|
-
def dispatch_error(err)
|
95
|
-
@response.status, rsph, @response.body = err.odata_get(@request)
|
96
|
-
headers rsph
|
97
|
-
end
|
98
|
-
|
99
|
-
def dispatch
|
100
|
-
req_ret = if @request.request_method !~ METHODS_REGEXP
|
101
|
-
[404, EMPTY_HASH, ['Did you get lost?']]
|
102
|
-
elsif @request.request_method == 'HEAD'
|
103
|
-
odata_head
|
104
|
-
else
|
105
|
-
dispatch_with_walker
|
106
|
-
end
|
107
|
-
@response.status, rsph, @response.body = req_ret
|
108
|
-
headers rsph
|
14
|
+
def initialize
|
15
|
+
# just get back the service base instance object
|
16
|
+
# that was saved on class level and save it here
|
17
|
+
# so it's not needed to call self.class.service
|
18
|
+
@service_base = self.class.get_service_base
|
109
19
|
end
|
110
20
|
|
111
21
|
def call(env)
|
112
|
-
|
113
|
-
dup._call(env)
|
114
|
-
end
|
115
|
-
|
116
|
-
def _call(env)
|
117
|
-
begin
|
118
|
-
@request = Safrano::Request.new(env)
|
119
|
-
@response = Safrano::Response.new
|
120
|
-
|
121
|
-
before.tap_error { |err| dispatch_error(err) }
|
122
|
-
.tap_valid { |_res| dispatch }
|
123
|
-
|
124
|
-
# handle remaining Sequel errors that we couldnt prevent with our
|
125
|
-
# own pre-checks
|
126
|
-
rescue Sequel::Error => e
|
127
|
-
dispatch_error(SequelExceptionError.new(e))
|
128
|
-
end
|
129
|
-
@response.finish
|
130
|
-
end
|
131
|
-
|
132
|
-
# Set multiple response headers with Hash.
|
133
|
-
def headers(hash = nil)
|
134
|
-
@response.headers.merge! hash if hash
|
135
|
-
@response.headers
|
22
|
+
Safrano::Request.new(env, @service_base).process
|
136
23
|
end
|
137
24
|
|
25
|
+
# needed for testing only ? try to remove this
|
138
26
|
def self.enable_batch
|
139
27
|
@service_base.enable_batch
|
140
28
|
end
|
141
29
|
|
30
|
+
# needed for testing only ? try to remove this
|
142
31
|
def self.path_prefix(path_pr)
|
143
32
|
@service_base.path_prefix path_pr
|
144
33
|
end
|
145
34
|
|
35
|
+
# needed for testing only ? try to remove this
|
146
36
|
def self.get_service_base
|
147
37
|
@service_base
|
148
38
|
end
|
@@ -157,6 +47,7 @@ module Safrano
|
|
157
47
|
sbase = Safrano::ServiceBase.new
|
158
48
|
sbase.instance_eval(&block) if block_given?
|
159
49
|
sbase.finalize_publishing
|
50
|
+
# save published service base instance on App-Class level
|
160
51
|
set_servicebase(sbase)
|
161
52
|
end
|
162
53
|
end
|
data/lib/safrano/request.rb
CHANGED
@@ -4,6 +4,49 @@ require 'rack'
|
|
4
4
|
require 'rfc2047'
|
5
5
|
|
6
6
|
module Safrano
|
7
|
+
# handle GET PUT etc
|
8
|
+
module MethodHandlers
|
9
|
+
def odata_options
|
10
|
+
@walker.finalize.tap_error { |err| return err.odata_get(self) }
|
11
|
+
.if_valid do |_context|
|
12
|
+
# cf. stackoverflow.com/questions/22924678/sinatra-delete-response-headers
|
13
|
+
headers.delete('Content-Type')
|
14
|
+
@response.headers.delete('Content-Type')
|
15
|
+
@response.headers['Content-Type'] = ''
|
16
|
+
[200, EMPTY_HASH, '']
|
17
|
+
end
|
18
|
+
end
|
19
|
+
|
20
|
+
def odata_delete
|
21
|
+
@walker.finalize.tap_error { |err| return err.odata_get(self) }
|
22
|
+
.if_valid { |context| context.odata_delete(self) }
|
23
|
+
end
|
24
|
+
|
25
|
+
def odata_put
|
26
|
+
@walker.finalize.tap_error { |err| return err.odata_get(self) }
|
27
|
+
.if_valid { |context| context.odata_put(self) }
|
28
|
+
end
|
29
|
+
|
30
|
+
def odata_patch
|
31
|
+
@walker.finalize.tap_error { |err| return err.odata_get(self) }
|
32
|
+
.if_valid { |context| context.odata_patch(self) }
|
33
|
+
end
|
34
|
+
|
35
|
+
def odata_get
|
36
|
+
@walker.finalize.tap_error { |err| return err.odata_get(self) }
|
37
|
+
.if_valid { |context| context.odata_get(self) }
|
38
|
+
end
|
39
|
+
|
40
|
+
def odata_post
|
41
|
+
@walker.finalize.tap_error { |err| return err.odata_get(self) }
|
42
|
+
.if_valid { |context| context.odata_post(self) }
|
43
|
+
end
|
44
|
+
|
45
|
+
def odata_head
|
46
|
+
[200, EMPTY_HASH, [EMPTY_STRING]]
|
47
|
+
end
|
48
|
+
end
|
49
|
+
|
7
50
|
# monkey patch deactivate Rack/multipart because it does not work on simple
|
8
51
|
# OData $batch requests when the content-length
|
9
52
|
# is not passed
|
@@ -13,6 +56,12 @@ module Safrano
|
|
13
56
|
HEADER_VAL_WITH_PAR = /(?:#{HEADER_VAL_RAW})\s*(?:;#{HEADER_PARAM})*/.freeze
|
14
57
|
ON_CGST_ERROR = (proc { |r| raise(Sequel::Rollback) if r.in_changeset })
|
15
58
|
|
59
|
+
METHODS_REGEXP = Regexp.new('HEAD|OPTIONS|GET|POST|PATCH|MERGE|PUT|DELETE').freeze
|
60
|
+
NOCACHE_HDRS = { 'Cache-Control' => 'no-cache',
|
61
|
+
'Expires' => '-1',
|
62
|
+
'Pragma' => 'no-cache' }.freeze
|
63
|
+
DATASERVICEVERSION = 'DataServiceVersion'
|
64
|
+
|
16
65
|
# borowed from Sinatra
|
17
66
|
class AcceptEntry
|
18
67
|
attr_accessor :params
|
@@ -31,10 +80,6 @@ module Safrano
|
|
31
80
|
@q = @params.delete('q') { 1.0 }.to_f
|
32
81
|
end
|
33
82
|
|
34
|
-
def method_missing(*args, &block)
|
35
|
-
to_str.send(*args, &block)
|
36
|
-
end
|
37
|
-
|
38
83
|
def <=>(other)
|
39
84
|
other.priority <=> priority
|
40
85
|
end
|
@@ -48,10 +93,6 @@ module Safrano
|
|
48
93
|
super || to_str.respond_to?(*args)
|
49
94
|
end
|
50
95
|
|
51
|
-
def to_s(full = false)
|
52
|
-
full ? entry : to_str
|
53
|
-
end
|
54
|
-
|
55
96
|
def to_str
|
56
97
|
@type
|
57
98
|
end
|
@@ -87,6 +128,82 @@ module Safrano
|
|
87
128
|
# content-id references map
|
88
129
|
attr_accessor :content_id_references
|
89
130
|
|
131
|
+
include MethodHandlers
|
132
|
+
|
133
|
+
def initialize(env, service_base)
|
134
|
+
super(env)
|
135
|
+
@service_base = service_base
|
136
|
+
end
|
137
|
+
|
138
|
+
# process the request and return finished response
|
139
|
+
def process
|
140
|
+
begin
|
141
|
+
@response = Safrano::Response.new
|
142
|
+
|
143
|
+
before.tap_error { |err| dispatch_error(err) }
|
144
|
+
.tap_valid { |_res| dispatch }
|
145
|
+
|
146
|
+
# handle remaining Sequel errors that we couldnt prevent with our
|
147
|
+
# own pre-checks
|
148
|
+
rescue Sequel::Error => e
|
149
|
+
dispatch_error(SequelExceptionError.new(e))
|
150
|
+
end
|
151
|
+
@response.finish
|
152
|
+
end
|
153
|
+
|
154
|
+
def before
|
155
|
+
negotiate_service_version.tap_valid do
|
156
|
+
myhdrs = NOCACHE_HDRS.dup
|
157
|
+
myhdrs[DATASERVICEVERSION] = @service.data_service_version
|
158
|
+
headers myhdrs
|
159
|
+
end
|
160
|
+
end
|
161
|
+
|
162
|
+
# dispatch for all methods requiring parsing of the path
|
163
|
+
# with walker (ie. allmost all excepted HEAD)
|
164
|
+
def dispatch_with_walker
|
165
|
+
@walker = create_odata_walker
|
166
|
+
case request_method
|
167
|
+
when 'GET'
|
168
|
+
odata_get
|
169
|
+
when 'POST'
|
170
|
+
odata_post
|
171
|
+
when 'DELETE'
|
172
|
+
odata_delete
|
173
|
+
when 'OPTIONS'
|
174
|
+
odata_options
|
175
|
+
when 'PUT'
|
176
|
+
odata_put
|
177
|
+
when 'PATCH', 'MERGE'
|
178
|
+
odata_patch
|
179
|
+
else
|
180
|
+
raise Error
|
181
|
+
end
|
182
|
+
end
|
183
|
+
|
184
|
+
def dispatch_error(err)
|
185
|
+
@response.status, rsph, @response.body = err.odata_get(self)
|
186
|
+
headers rsph
|
187
|
+
end
|
188
|
+
|
189
|
+
def dispatch
|
190
|
+
req_ret = if request_method !~ METHODS_REGEXP
|
191
|
+
[404, EMPTY_HASH, ['Did you get lost?']]
|
192
|
+
elsif request_method == 'HEAD'
|
193
|
+
odata_head
|
194
|
+
else
|
195
|
+
dispatch_with_walker
|
196
|
+
end
|
197
|
+
@response.status, rsph, @response.body = req_ret
|
198
|
+
headers rsph
|
199
|
+
end
|
200
|
+
|
201
|
+
# Set multiple response headers with Hash.
|
202
|
+
def headers(hash = nil)
|
203
|
+
@response.headers.merge! hash if hash
|
204
|
+
@response.headers
|
205
|
+
end
|
206
|
+
|
90
207
|
# stores the newly created entity for the current content-id of
|
91
208
|
# the processed request
|
92
209
|
def register_content_id_ref(new_entity)
|
@@ -213,6 +330,7 @@ module Safrano
|
|
213
330
|
MIN_DTSV_PARSE_ERROR = Safrano::BadRequestError.new(
|
214
331
|
'MinDataServiceVersion could not be parsed'
|
215
332
|
).freeze
|
333
|
+
|
216
334
|
def get_minversion
|
217
335
|
if (rqv = env['HTTP_MINDATASERVICEVERSION'])
|
218
336
|
if (m = DATASERVICEVERSION_RGX.match(rqv))
|
@@ -234,6 +352,7 @@ module Safrano
|
|
234
352
|
MAX_LT_MIN_DTSV_ERROR = Safrano::BadRequestError.new(
|
235
353
|
'MinDataServiceVersion is larger as MaxDataServiceVersion'
|
236
354
|
).freeze
|
355
|
+
|
237
356
|
def negotiate_service_version
|
238
357
|
get_maxversion.if_valid do |maxv|
|
239
358
|
get_minversion.if_valid do |minv|
|
data/lib/safrano/service.rb
CHANGED
@@ -257,7 +257,7 @@ module Safrano
|
|
257
257
|
# * Single/Multi PK
|
258
258
|
# * Media/Non-Media entity
|
259
259
|
# Putting this logic here in modules loaded once on start shall result in less runtime overhead
|
260
|
-
def register_model(modelklass, entity_set_name = nil, is_media
|
260
|
+
def register_model(modelklass, entity_set_name = nil, is_media: false)
|
261
261
|
# check that the provided klass is a Sequel Model
|
262
262
|
|
263
263
|
raise(Safrano::API::ModelNameError, modelklass) unless modelklass.is_a? Sequel::Model::ClassMethods
|
@@ -314,7 +314,7 @@ module Safrano
|
|
314
314
|
end
|
315
315
|
|
316
316
|
def publish_media_model(modelklass, entity_set_name = nil, &block)
|
317
|
-
register_model(modelklass, entity_set_name, true)
|
317
|
+
register_model(modelklass, entity_set_name, is_media: true)
|
318
318
|
# we need to execute the passed block in a deferred step
|
319
319
|
# after all models have been registered (due to rel. dependancies)
|
320
320
|
# modelklass.instance_eval(&block) if block_given?
|
@@ -450,7 +450,7 @@ module Safrano
|
|
450
450
|
end
|
451
451
|
|
452
452
|
# check function import definition
|
453
|
-
function_imports.each_value
|
453
|
+
function_imports.each_value(&:check_definition)
|
454
454
|
end
|
455
455
|
|
456
456
|
def execute_deferred_iblocks
|
data/lib/safrano/version.rb
CHANGED
@@ -99,6 +99,8 @@ class JoinByPathsHelper < Set
|
|
99
99
|
@result << iset
|
100
100
|
end
|
101
101
|
|
102
|
+
many_to_many_count = 0 # counter for differing multiple
|
103
|
+
# many_to_many through alias
|
102
104
|
@result.map! do |jseg|
|
103
105
|
jseg.map do |seg|
|
104
106
|
leftm = seg.first.model_class
|
@@ -122,29 +124,65 @@ class JoinByPathsHelper < Set
|
|
122
124
|
lks = [leftm.primary_key].flatten
|
123
125
|
rks = [assoc[:key]].flatten
|
124
126
|
|
125
|
-
|
126
|
-
#
|
127
|
+
when :many_to_many, :one_through_one
|
128
|
+
# :many_to_many :: A join table is used that has a foreign key that points
|
127
129
|
# to this model's primary key and a foreign key that points to the
|
128
130
|
# associated model's primary key. Each current model object can be
|
129
131
|
# associated with many associated model objects, and each associated
|
130
132
|
# model object can be associated with many current model objects.
|
133
|
+
# TODO: testcase for :one_through_one
|
131
134
|
# when # :one_through_one :: Similar to many_to_many in terms of foreign keys, but only one object
|
132
135
|
# is associated to the current object through the association.
|
133
136
|
# Provides only getter methods, no setter or modification methods.
|
134
137
|
|
138
|
+
result_ = []
|
139
|
+
|
140
|
+
# in case of multiple many_to_many rels, we need differing through aliases
|
141
|
+
many_to_many_count = many_to_many_count + 1
|
142
|
+
through_alias = "t#{many_to_many_count}".to_sym
|
143
|
+
|
144
|
+
# For many_to_many first we add the join with assigment table
|
145
|
+
lks_ = [assoc[:left_primary_key]].flatten
|
146
|
+
rks_ = [assoc[:left_key]].flatten
|
147
|
+
|
148
|
+
lks_.map! { |k| Sequel[seg.first.alias_sym][k] } unless seg.first.empty?
|
149
|
+
rks_.map! { |k| Sequel[through_alias][k] }
|
150
|
+
|
151
|
+
result_ << {
|
152
|
+
type: assoc[:type],
|
153
|
+
left: leftm.table_name,
|
154
|
+
right: assoc[:join_table],
|
155
|
+
alias: through_alias,
|
156
|
+
cond: rks_.zip(lks_).to_h
|
157
|
+
}
|
158
|
+
|
159
|
+
# then we add the join with with target table (right)
|
160
|
+
lks = [assoc[:right_key]].flatten
|
161
|
+
rks = [assoc.right_primary_key].flatten
|
162
|
+
|
163
|
+
lks.map! { |k| Sequel[through_alias][k] }
|
164
|
+
rks.map! { |k| Sequel[seg.last.alias_sym][k] }
|
165
|
+
|
166
|
+
result_ << {
|
167
|
+
type: assoc[:type],
|
168
|
+
left: assoc[:join_table],
|
169
|
+
right: rightm.table_name,
|
170
|
+
alias: seg.last.alias_sym,
|
171
|
+
cond: rks.zip(lks).to_h
|
172
|
+
}
|
173
|
+
|
174
|
+
next result_
|
175
|
+
|
176
|
+
|
135
177
|
end
|
136
178
|
|
137
179
|
lks.map! { |k| Sequel[seg.first.alias_sym][k] } unless seg.first.empty?
|
138
|
-
|
139
180
|
rks.map! { |k| Sequel[seg.last.alias_sym][k] }
|
140
181
|
|
141
182
|
{ type: assoc[:type],
|
142
|
-
segment: seg,
|
143
183
|
left: leftm.table_name,
|
144
184
|
right: rightm.table_name,
|
145
185
|
alias: seg.last.alias_sym,
|
146
|
-
left_keys: lks,
|
147
|
-
right_keys: rks,
|
148
186
|
cond: rks.zip(lks).to_h }
|
149
187
|
end
|
150
188
|
end
|
@@ -159,11 +197,14 @@ class JoinByPathsHelper < Set
|
|
159
197
|
return start_dataset if empty?
|
160
198
|
|
161
199
|
build_unique_join_segments
|
162
|
-
|
200
|
+
need_distinct = false
|
201
|
+
ret = @result.flatten.inject(start_dataset) do |dt, jo|
|
202
|
+
need_distinct = true if jo[:type] == :many_to_many
|
163
203
|
dt.left_join(Sequel[jo[:right]].as(jo[:alias]),
|
164
204
|
jo[:cond],
|
165
205
|
implicit_qualifier: jo[:left])
|
166
206
|
end
|
207
|
+
need_distinct ? ret.distinct : ret
|
167
208
|
end
|
168
209
|
|
169
210
|
def join_by_paths_helper(*pathlist)
|
metadata
CHANGED
@@ -1,43 +1,55 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: safrano
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 0.
|
4
|
+
version: 0.7.0
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- oz
|
8
8
|
autorequire:
|
9
9
|
bindir: bin
|
10
10
|
cert_chain: []
|
11
|
-
date: 2023-02
|
11
|
+
date: 2023-09-02 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
14
|
name: rack
|
15
15
|
requirement: !ruby/object:Gem::Requirement
|
16
16
|
requirements:
|
17
|
-
- - "
|
17
|
+
- - ">="
|
18
18
|
- !ruby/object:Gem::Version
|
19
19
|
version: '2.0'
|
20
|
+
- - "<"
|
21
|
+
- !ruby/object:Gem::Version
|
22
|
+
version: '4.0'
|
20
23
|
type: :runtime
|
21
24
|
prerelease: false
|
22
25
|
version_requirements: !ruby/object:Gem::Requirement
|
23
26
|
requirements:
|
24
|
-
- - "
|
27
|
+
- - ">="
|
25
28
|
- !ruby/object:Gem::Version
|
26
29
|
version: '2.0'
|
30
|
+
- - "<"
|
31
|
+
- !ruby/object:Gem::Version
|
32
|
+
version: '4.0'
|
27
33
|
- !ruby/object:Gem::Dependency
|
28
34
|
name: rack-cors
|
29
35
|
requirement: !ruby/object:Gem::Requirement
|
30
36
|
requirements:
|
31
|
-
- - "
|
37
|
+
- - ">="
|
32
38
|
- !ruby/object:Gem::Version
|
33
39
|
version: '1.1'
|
40
|
+
- - "<"
|
41
|
+
- !ruby/object:Gem::Version
|
42
|
+
version: '3.0'
|
34
43
|
type: :runtime
|
35
44
|
prerelease: false
|
36
45
|
version_requirements: !ruby/object:Gem::Requirement
|
37
46
|
requirements:
|
38
|
-
- - "
|
47
|
+
- - ">="
|
39
48
|
- !ruby/object:Gem::Version
|
40
49
|
version: '1.1'
|
50
|
+
- - "<"
|
51
|
+
- !ruby/object:Gem::Version
|
52
|
+
version: '3.0'
|
41
53
|
- !ruby/object:Gem::Dependency
|
42
54
|
name: rfc2047
|
43
55
|
requirement: !ruby/object:Gem::Requirement
|