safrano 0.6.6 → 0.6.8
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/odata/batch.rb +39 -33
- data/lib/odata/collection_media.rb +90 -78
- data/lib/odata/entity.rb +1 -2
- data/lib/odata/function_import.rb +55 -13
- data/lib/odata/model_ext.rb +8 -8
- data/lib/odata/request/json.rb +7 -1
- data/lib/safrano/core.rb +4 -4
- data/lib/safrano/multipart.rb +20 -18
- data/lib/safrano/rack_app.rb +14 -123
- data/lib/safrano/request.rb +126 -0
- data/lib/safrano/service.rb +9 -2
- data/lib/safrano/version.rb +1 -1
- metadata +22 -10
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 9d1ee63cc31ce50cff784481ce75f5f5581c2b9ff406a18efb516908bbf7c65d
|
4
|
+
data.tar.gz: 5770021472ccce8efb445114d4b4834b4fb0ff31b58f6444654cfb16b5aeb860
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: b1be48927ce7f3b5350ea903fa6ebb514cd6c09a1eafb064b7760c56bb396f02e59cab6e2a2e091ab8c526c77d45e9ff18838728d34d7500b29f611ba98e858b
|
7
|
+
data.tar.gz: 5877c1e41a0bbbcb76d948914a703a24ea8cdf52215815b7c4051c3cfb3807463adfa0144ccce4861dfd988abf8776a002de6353d6343205f3767af02b9ca090
|
data/lib/odata/batch.rb
CHANGED
@@ -8,54 +8,63 @@ require_relative './common_logger'
|
|
8
8
|
module Safrano
|
9
9
|
# Support for OData multipart $batch Requests
|
10
10
|
class Request
|
11
|
-
def create_batch_app
|
12
|
-
Batch::MyOApp.new(self)
|
13
|
-
end
|
14
|
-
|
15
11
|
def parse_multipart
|
16
12
|
@mimep = MIME::Media::Parser.new
|
17
13
|
@boundary = media_type_params['boundary']
|
18
14
|
@mimep.hook_multipart(media_type, @boundary)
|
19
15
|
@mimep.parse_str(body)
|
20
16
|
end
|
17
|
+
|
18
|
+
# The top-level (full_req) Request is used like
|
19
|
+
# a Rack App
|
20
|
+
# With this method we get the response of part-requests
|
21
|
+
# app.call(env) --> full_req.bach_call(part_req)
|
22
|
+
|
23
|
+
def batch_call(part_req)
|
24
|
+
Safrano::Batch::PartRequest.new(part_req, self).process
|
25
|
+
end
|
26
|
+
|
27
|
+
# needed for changeset transaction
|
28
|
+
def db
|
29
|
+
@service.collections.first.db
|
30
|
+
end
|
21
31
|
end
|
22
32
|
|
23
33
|
module Batch
|
24
|
-
#
|
25
|
-
class
|
26
|
-
|
27
|
-
|
28
|
-
|
29
|
-
|
30
|
-
def initialize(full_req)
|
34
|
+
# Part-Request part of a Batch-Request
|
35
|
+
class PartRequest < Safrano::Request
|
36
|
+
def initialize(part_req, full_req)
|
37
|
+
batch_env(part_req, full_req)
|
38
|
+
@env['HTTP_HOST'] = full_req.env['HTTP_HOST']
|
39
|
+
super(@env, full_req.service_base)
|
31
40
|
@full_req = full_req
|
32
|
-
@
|
41
|
+
@part_req = part_req
|
33
42
|
end
|
34
43
|
|
35
44
|
# redefined for $batch
|
36
45
|
def before
|
37
46
|
headers 'Cache-Control' => 'no-cache'
|
38
|
-
@
|
39
|
-
headers 'DataServiceVersion' => @
|
47
|
+
@service = @full_req.service
|
48
|
+
headers 'DataServiceVersion' => @service.data_service_version
|
40
49
|
end
|
41
50
|
|
42
|
-
def
|
43
|
-
env = batch_env(part_req)
|
44
|
-
env['HTTP_HOST'] = @full_req.env['HTTP_HOST']
|
51
|
+
def process
|
45
52
|
began_at = Rack::Utils.clock_time
|
46
|
-
|
53
|
+
|
47
54
|
@response = Safrano::Response.new
|
48
55
|
|
49
|
-
if part_req.level == 2
|
50
|
-
@
|
51
|
-
@
|
52
|
-
@
|
56
|
+
if @part_req.level == 2
|
57
|
+
@in_changeset = true
|
58
|
+
@content_id = @part_req.content_id
|
59
|
+
@content_id_references = @part_req.content_id_references
|
53
60
|
end
|
54
61
|
|
55
62
|
before
|
63
|
+
|
56
64
|
dispatch
|
57
65
|
|
58
66
|
status, header, body = @response.finish
|
67
|
+
|
59
68
|
# Logging of sub-requests with ODataCommonLogger.
|
60
69
|
# A bit hacky but working
|
61
70
|
# TODO: test ?
|
@@ -80,19 +89,19 @@ module Safrano
|
|
80
89
|
converted_headers
|
81
90
|
end
|
82
91
|
|
83
|
-
def batch_env(mime_req)
|
92
|
+
def batch_env(mime_req, full_req)
|
84
93
|
@env = ::Rack::MockRequest.env_for(mime_req.uri,
|
85
94
|
method: mime_req.http_method,
|
86
95
|
input: mime_req.content)
|
87
96
|
# Logging of sub-requests
|
88
|
-
@env[Rack::RACK_ERRORS] =
|
97
|
+
@env[Rack::RACK_ERRORS] = full_req.env[Rack::RACK_ERRORS]
|
89
98
|
@env.merge! headers_for_env(mime_req.hd)
|
90
99
|
|
91
100
|
@env
|
92
101
|
end
|
93
102
|
end
|
94
103
|
|
95
|
-
#
|
104
|
+
# $batch Handler
|
96
105
|
class HandlerBase
|
97
106
|
TREND = Safrano::Transition.new('', trans: 'transition_end')
|
98
107
|
def allowed_transitions
|
@@ -103,7 +112,8 @@ module Safrano
|
|
103
112
|
Safrano::Transition::RESULT_END
|
104
113
|
end
|
105
114
|
end
|
106
|
-
|
115
|
+
|
116
|
+
# $batch disabled Handler
|
107
117
|
class DisabledHandler < HandlerBase
|
108
118
|
def odata_post(_req)
|
109
119
|
[404, EMPTY_HASH, '$batch is not enabled ']
|
@@ -113,7 +123,8 @@ module Safrano
|
|
113
123
|
[404, EMPTY_HASH, '$batch is not enabled ']
|
114
124
|
end
|
115
125
|
end
|
116
|
-
|
126
|
+
|
127
|
+
# $batch enabled Handler
|
117
128
|
class EnabledHandler < HandlerBase
|
118
129
|
attr_accessor :boundary
|
119
130
|
attr_accessor :mmboundary
|
@@ -121,8 +132,6 @@ module Safrano
|
|
121
132
|
attr_accessor :parts
|
122
133
|
attr_accessor :request
|
123
134
|
|
124
|
-
def initialize; end
|
125
|
-
|
126
135
|
# here we are in the Batch handler object, and this POST should
|
127
136
|
# normally handle a $batch request
|
128
137
|
def odata_post(req)
|
@@ -130,15 +139,12 @@ module Safrano
|
|
130
139
|
|
131
140
|
if @request.media_type == Safrano::MP_MIXED
|
132
141
|
|
133
|
-
batcha = @request.create_batch_app
|
134
142
|
@mult_request = @request.parse_multipart
|
135
143
|
|
136
144
|
@mult_request.prepare_content_id_refs
|
137
|
-
@mult_response = Safrano::Response.new
|
138
145
|
|
139
|
-
|
146
|
+
@mult_request.get_mult_resp(@request)
|
140
147
|
|
141
|
-
[202, resp_hdrs, @mult_response.body[0]]
|
142
148
|
else
|
143
149
|
[415, EMPTY_HASH, 'Unsupported Media Type']
|
144
150
|
end
|
@@ -2,6 +2,8 @@
|
|
2
2
|
|
3
3
|
require 'rack'
|
4
4
|
require 'fileutils'
|
5
|
+
require 'tempfile'
|
6
|
+
require 'pathname'
|
5
7
|
require_relative './navigation_attribute'
|
6
8
|
|
7
9
|
module Safrano
|
@@ -14,37 +16,60 @@ module Safrano
|
|
14
16
|
Contract::OK
|
15
17
|
end
|
16
18
|
end
|
17
|
-
|
18
19
|
# Simple static File/Directory based media store handler
|
19
20
|
# similar to Rack::Static
|
20
21
|
# with a flat directory structure
|
21
22
|
class Static < Handler
|
22
23
|
def initialize(root: nil, mediaklass:)
|
23
|
-
@root = File.absolute_path(root || Dir.pwd)
|
24
|
-
@
|
24
|
+
@root = Pathname(File.absolute_path(root || Dir.pwd))
|
25
|
+
@files_class = (::Rack.release[0..2] == '2.0') ? ::Rack::File : ::Rack::Files
|
25
26
|
@media_class = mediaklass
|
26
|
-
@media_dir_name = mediaklass.to_s
|
27
|
+
@media_dir_name = Pathname(mediaklass.to_s)
|
28
|
+
@semaphore = Thread::Mutex.new
|
29
|
+
|
27
30
|
register
|
28
31
|
end
|
29
32
|
|
30
33
|
def register
|
31
|
-
@abs_klass_dir =
|
34
|
+
@abs_klass_dir = @root + @media_dir_name
|
35
|
+
@abs_temp_dir = @abs_klass_dir + 'tmp'
|
32
36
|
end
|
33
37
|
|
34
38
|
def create_abs_class_dir
|
35
39
|
FileUtils.makedirs @abs_klass_dir unless Dir.exist?(@abs_klass_dir)
|
36
40
|
end
|
37
41
|
|
42
|
+
def create_abs_temp_dir
|
43
|
+
FileUtils.makedirs @abs_temp_dir unless Dir.exist?(@abs_temp_dir)
|
44
|
+
end
|
45
|
+
|
38
46
|
def finalize
|
39
47
|
create_abs_class_dir
|
48
|
+
create_abs_temp_dir
|
49
|
+
end
|
50
|
+
|
51
|
+
# see also ...
|
52
|
+
# File activesupport/lib/active_support/core_ext/file/atomic.rb, line 21
|
53
|
+
def atomic_write(file_name)
|
54
|
+
Tempfile.open('', @abs_temp_dir) do |temp_file|
|
55
|
+
temp_file.binmode
|
56
|
+
return_val = yield temp_file
|
57
|
+
temp_file.close
|
58
|
+
|
59
|
+
# Overwrite original file with temp file
|
60
|
+
File.rename(temp_file.path, file_name)
|
61
|
+
return_val
|
62
|
+
end
|
40
63
|
end
|
41
64
|
|
42
65
|
# minimal working implementation...
|
43
|
-
# Note:
|
66
|
+
# Note: files_app works relative to @root directory
|
44
67
|
def odata_get(request:, entity:)
|
45
68
|
media_env = request.env.dup
|
46
69
|
media_env['PATH_INFO'] = filename(entity)
|
47
|
-
|
70
|
+
# new app instance for each call for thread safety
|
71
|
+
files_app = @files_class.new(@root)
|
72
|
+
fsret = files_app.call(media_env)
|
48
73
|
if fsret.first == 200
|
49
74
|
# provide own content type as we keep it in the media entity
|
50
75
|
fsret[1]['Content-Type'] = entity.content_type
|
@@ -55,28 +80,23 @@ module Safrano
|
|
55
80
|
# this is relative to @root
|
56
81
|
# eg. Photo/1
|
57
82
|
def media_path(entity)
|
58
|
-
|
83
|
+
@media_dir_name + media_directory(entity)
|
59
84
|
end
|
60
85
|
|
61
86
|
# relative to @root
|
62
87
|
# eg Photo/1/1
|
63
88
|
def filename(entity)
|
64
|
-
|
65
|
-
# simple design: one file per directory, and the directory
|
66
|
-
# contains the media entity-id --> implicit link between the media
|
67
|
-
# entity
|
68
|
-
File.join(media_path(entity), Dir.glob('*').max)
|
69
|
-
end
|
89
|
+
media_path(entity) + ressource_version(entity)
|
70
90
|
end
|
71
91
|
|
72
92
|
# /@root/Photo/1
|
73
93
|
def abs_path(entity)
|
74
|
-
|
94
|
+
@root + media_path(entity)
|
75
95
|
end
|
76
96
|
|
77
97
|
# absolute filename
|
78
98
|
def abs_filename(entity)
|
79
|
-
|
99
|
+
@root + filename(entity)
|
80
100
|
end
|
81
101
|
|
82
102
|
# this is relative to abs_klass_dir(entity) eg to /@root/Photo
|
@@ -86,29 +106,31 @@ module Safrano
|
|
86
106
|
entity.media_path_id
|
87
107
|
end
|
88
108
|
|
89
|
-
|
90
|
-
|
109
|
+
# the same as above but absolute
|
110
|
+
def abs_media_directory(entity)
|
111
|
+
@abs_klass_dir + entity.media_path_id
|
112
|
+
end
|
113
|
+
|
114
|
+
# yields the absolute path of media directory
|
115
|
+
# and ensure the directory exists
|
116
|
+
def with_media_directory(entity)
|
117
|
+
mpi = abs_media_directory(entity)
|
91
118
|
Dir.mkdir mpi unless Dir.exist?(mpi)
|
92
|
-
|
93
|
-
yield
|
94
|
-
end
|
119
|
+
yield Pathname(mpi)
|
95
120
|
end
|
96
121
|
|
97
122
|
def odata_delete(entity:)
|
98
|
-
|
99
|
-
|
100
|
-
|
101
|
-
|
102
|
-
end
|
123
|
+
mpi = abs_media_directory(entity)
|
124
|
+
return unless Dir.exist?(mpi)
|
125
|
+
|
126
|
+
mpi.children.each { |oldp| File.delete(oldp) }
|
103
127
|
end
|
104
128
|
|
105
129
|
# Here as well, MVP implementation
|
106
130
|
def save_file(data:, filename:, entity:)
|
107
|
-
|
108
|
-
|
109
|
-
|
110
|
-
File.open(filename, 'wb') { |f| IO.copy_stream(data, f) }
|
111
|
-
end
|
131
|
+
with_media_directory(entity) do |d|
|
132
|
+
filename = d + '1'
|
133
|
+
atomic_write(filename) { |f| IO.copy_stream(data, f) }
|
112
134
|
end
|
113
135
|
end
|
114
136
|
|
@@ -116,24 +138,31 @@ module Safrano
|
|
116
138
|
# after each upload, so that clients get informed about new versions
|
117
139
|
# of the same media ressource
|
118
140
|
def ressource_version(entity)
|
119
|
-
|
120
|
-
in_media_directory(entity) do
|
121
|
-
Dir.glob('*').max
|
122
|
-
end
|
123
|
-
end
|
141
|
+
abs_media_directory(entity).children(with_directory = false).max.to_s
|
124
142
|
end
|
125
143
|
|
126
|
-
#
|
127
|
-
def replace_file(data:,
|
128
|
-
|
129
|
-
|
130
|
-
|
131
|
-
|
132
|
-
|
133
|
-
|
134
|
-
|
135
|
-
|
136
|
-
|
144
|
+
# Note: add a new Version and remove the previous one
|
145
|
+
def replace_file(data:, entity:)
|
146
|
+
with_media_directory(entity) do |d|
|
147
|
+
tp = Tempfile.open('', @abs_temp_dir) do |temp_file|
|
148
|
+
temp_file.binmode
|
149
|
+
IO.copy_stream(data, temp_file)
|
150
|
+
temp_file.path
|
151
|
+
end
|
152
|
+
|
153
|
+
# picking new filename and the "move" operation must
|
154
|
+
# be protected
|
155
|
+
@semaphore.synchronize do
|
156
|
+
# new filename = "version" + 1
|
157
|
+
v = ressource_version(entity)
|
158
|
+
filename = d + (v.to_i + 1).to_s
|
159
|
+
|
160
|
+
# Move temp file to original target file
|
161
|
+
File.rename(tp, filename)
|
162
|
+
|
163
|
+
# remove the previous version
|
164
|
+
filename = d + v
|
165
|
+
File.delete(filename)
|
137
166
|
end
|
138
167
|
end
|
139
168
|
end
|
@@ -162,42 +191,25 @@ module Safrano
|
|
162
191
|
StaticTree.path_builder(entity.media_path_ids)
|
163
192
|
end
|
164
193
|
|
165
|
-
|
166
|
-
|
194
|
+
# the same as above but absolute
|
195
|
+
def abs_media_directory(entity)
|
196
|
+
@abs_klass_dir + StaticTree.path_builder(entity.media_path_ids)
|
197
|
+
end
|
198
|
+
|
199
|
+
# yields the absolute path of media directory
|
200
|
+
# and ensure the directory exists
|
201
|
+
def with_media_directory(entity)
|
202
|
+
mpi = abs_media_directory(entity)
|
203
|
+
|
167
204
|
FileUtils.makedirs mpi unless Dir.exist?(mpi)
|
168
|
-
|
205
|
+
yield Pathname(mpi)
|
169
206
|
end
|
170
207
|
|
171
208
|
def odata_delete(entity:)
|
172
|
-
|
173
|
-
|
174
|
-
Dir.glob('*').sort.each { |oldf| File.delete(oldf) if File.file?(oldf) }
|
175
|
-
end
|
176
|
-
end
|
177
|
-
end
|
209
|
+
mpi = abs_media_directory(entity)
|
210
|
+
return unless Dir.exist?(mpi)
|
178
211
|
|
179
|
-
|
180
|
-
# def replace_file(data:, filename:, entity:)
|
181
|
-
# Dir.chdir(abs_klass_dir(entity)) do
|
182
|
-
# in_media_directory(entity) do
|
183
|
-
# Dir.glob('*').each { |oldf| File.delete(oldf) if File.file?(oldf) }
|
184
|
-
# File.open(filename, 'wb') { |f| IO.copy_stream(data, f) }
|
185
|
-
# end
|
186
|
-
# end
|
187
|
-
# end
|
188
|
-
# Here as well, MVP implementation
|
189
|
-
def replace_file(data:, filename:, entity:)
|
190
|
-
Dir.chdir(@abs_klass_dir) do
|
191
|
-
in_media_directory(entity) do
|
192
|
-
version = nil
|
193
|
-
Dir.glob('*').sort.each do |oldf|
|
194
|
-
version = oldf
|
195
|
-
File.delete(oldf)
|
196
|
-
end
|
197
|
-
filename = (version.to_i + 1).to_s
|
198
|
-
File.open(filename, 'wb') { |f| IO.copy_stream(data, f) }
|
199
|
-
end
|
200
|
-
end
|
212
|
+
mpi.children.each { |oldp| File.delete(oldp) if File.file?(oldp) }
|
201
213
|
end
|
202
214
|
end
|
203
215
|
end
|
data/lib/odata/entity.rb
CHANGED
@@ -10,6 +10,21 @@ module Safrano
|
|
10
10
|
end
|
11
11
|
|
12
12
|
module FunctionImport
|
13
|
+
# error classes
|
14
|
+
class DefinitionMissing < StandardError
|
15
|
+
def initialize(fnam)
|
16
|
+
msg = "Function import #{fnam}: definition is missing. Provide definition either as a return code block or with .definition(lambda)"
|
17
|
+
super(msg)
|
18
|
+
end
|
19
|
+
end
|
20
|
+
class ProcRedefinition < StandardError
|
21
|
+
def initialize(fnam)
|
22
|
+
msg = "Function import #{fnam}: Block/lambda Redefinition . Provide definition either as a return code block or with .definition(lambda) but not both"
|
23
|
+
super(msg)
|
24
|
+
end
|
25
|
+
end
|
26
|
+
|
27
|
+
# Function import object
|
13
28
|
class Function
|
14
29
|
@allowed_transitions = [Safrano::TransitionEnd]
|
15
30
|
attr_reader :name
|
@@ -18,7 +33,6 @@ module Safrano
|
|
18
33
|
def initialize(name)
|
19
34
|
@name = name
|
20
35
|
@http_method = 'GET'
|
21
|
-
@use_contract = false
|
22
36
|
end
|
23
37
|
|
24
38
|
def allowed_transitions
|
@@ -53,13 +67,7 @@ module Safrano
|
|
53
67
|
end
|
54
68
|
alias auto_query_params auto_query_parameters
|
55
69
|
|
56
|
-
def use_contract
|
57
|
-
@use_contract = true
|
58
|
-
end
|
59
|
-
|
60
70
|
def return(klassmod, &proc)
|
61
|
-
raise('Please provide a code block') unless block_given?
|
62
|
-
|
63
71
|
@returning = if klassmod.respond_to? :return_as_instance_descriptor
|
64
72
|
klassmod.return_as_instance_descriptor
|
65
73
|
else
|
@@ -67,13 +75,35 @@ module Safrano
|
|
67
75
|
# --> assume it is a Primitive
|
68
76
|
ResultDefinition.asPrimitiveType(klassmod)
|
69
77
|
end
|
70
|
-
|
78
|
+
# block is optional since 0.6.7
|
79
|
+
# the function definition can now also be made with .definition(lambda)
|
80
|
+
# consistency check that there is a single definition either as a
|
81
|
+
# return-block or a definition lambda is made on publish finalise
|
82
|
+
|
83
|
+
if block_given?
|
84
|
+
# proc already defined...
|
85
|
+
raise Redefinition.new(@name) if @proc
|
86
|
+
|
87
|
+
@proc = proc
|
88
|
+
end
|
89
|
+
|
71
90
|
self
|
72
91
|
end
|
73
92
|
|
74
|
-
def
|
75
|
-
raise('Please provide a
|
93
|
+
def definition(lambda)
|
94
|
+
raise('Please provide a lambda') unless lambda
|
95
|
+
# proc already defined...
|
96
|
+
raise ProcRedefinition.new(@name) if @proc
|
97
|
+
|
98
|
+
@proc = lambda
|
99
|
+
end
|
100
|
+
|
101
|
+
# this is called from service.finalize_publishing
|
102
|
+
def check_definition
|
103
|
+
raise DefinitionMissing.new(@name) unless @proc
|
104
|
+
end
|
76
105
|
|
106
|
+
def return_collection(klassmod, lambda: nil, &proc)
|
77
107
|
@returning = if klassmod.respond_to? :return_as_collection_descriptor
|
78
108
|
klassmod.return_as_collection_descriptor
|
79
109
|
else
|
@@ -82,7 +112,14 @@ module Safrano
|
|
82
112
|
# ResultAsPrimitiveTypeColl.new(klassmod)
|
83
113
|
ResultDefinition.asPrimitiveTypeColl(klassmod)
|
84
114
|
end
|
85
|
-
|
115
|
+
# block is optional since 0.6.7
|
116
|
+
if block_given?
|
117
|
+
# proc already defined...
|
118
|
+
raise ProcRedefinition.new(@name) if @proc
|
119
|
+
|
120
|
+
@proc = proc
|
121
|
+
end
|
122
|
+
|
86
123
|
self
|
87
124
|
end
|
88
125
|
# def initialize_params
|
@@ -156,8 +193,13 @@ module Safrano
|
|
156
193
|
def with_transition_validated(req)
|
157
194
|
# initialize_params
|
158
195
|
@params = req.params
|
159
|
-
|
160
|
-
|
196
|
+
unless (@error = check_url_func_params)
|
197
|
+
begin
|
198
|
+
return yield
|
199
|
+
rescue LocalJumpError => e
|
200
|
+
@error = Safrano::ServiceOperationReturnError.new
|
201
|
+
end
|
202
|
+
end
|
161
203
|
[nil, :error, @error] if @error
|
162
204
|
end
|
163
205
|
|
data/lib/odata/model_ext.rb
CHANGED
@@ -445,9 +445,9 @@ module Safrano
|
|
445
445
|
end # db_schema.each do |col, props|
|
446
446
|
|
447
447
|
# check if key needs casting. Important for later entity-uri generation !
|
448
|
-
|
449
|
-
|
450
|
-
|
448
|
+
return unless primary_key.is_a? Symbol # single key field
|
449
|
+
|
450
|
+
@pk_castfunc = @casted_cols[primary_key]
|
451
451
|
end # build_casted_cols(service)
|
452
452
|
|
453
453
|
def finalize_publishing(service)
|
@@ -516,7 +516,6 @@ module Safrano
|
|
516
516
|
|
517
517
|
@iuk_rgx_parts.transform_values! { |v| /\A#{v}\z/ }
|
518
518
|
|
519
|
-
@entity_id_url_regexp = KEYPRED_URL_REGEXP
|
520
519
|
else
|
521
520
|
@pk_names = [primary_key.to_s]
|
522
521
|
@pk_cast_from_string = nil
|
@@ -543,9 +542,9 @@ module Safrano
|
|
543
542
|
end
|
544
543
|
end
|
545
544
|
@iuk_rgx = /\A\s*#{kvpredicate}\s*\z/
|
546
|
-
# @entity_id_url_regexp = /\A\(\s*#{kvpredicate}\s*\)(.*)/.freeze
|
547
|
-
@entity_id_url_regexp = KEYPRED_URL_REGEXP
|
548
545
|
end
|
546
|
+
# @entity_id_url_regexp = /\A\(\s*#{kvpredicate}\s*\)(.*)/.freeze
|
547
|
+
@entity_id_url_regexp = KEYPRED_URL_REGEXP
|
549
548
|
end
|
550
549
|
|
551
550
|
def prepare_fields
|
@@ -673,7 +672,7 @@ module Safrano
|
|
673
672
|
|
674
673
|
mid.split(/\s*,\s*/).each do |midpart|
|
675
674
|
mval = nil
|
676
|
-
mpk,
|
675
|
+
mpk, _mrgx = scan_rgx_parts.find do |_pk, rgx|
|
677
676
|
if (md = rgx.match(midpart))
|
678
677
|
mval = md[1]
|
679
678
|
end
|
@@ -753,7 +752,8 @@ module Safrano
|
|
753
752
|
# json is default content type so we dont need to specify it here again
|
754
753
|
# TODO quirks array mode !
|
755
754
|
# [201, EMPTY_HASH, new_entity.to_odata_post_json(service: req.service)]
|
756
|
-
[201, {
|
755
|
+
[201, { Safrano::LOCATION => new_entity.uri },
|
756
|
+
new_entity.to_odata_create_json(request: req)]
|
757
757
|
else # TODO: other formats
|
758
758
|
415
|
759
759
|
end
|
data/lib/odata/request/json.rb
CHANGED
@@ -1,9 +1,15 @@
|
|
1
1
|
require 'json'
|
2
2
|
require 'time'
|
3
|
-
|
3
|
+
require 'base64'
|
4
4
|
# client parsing functionality to ease testing
|
5
5
|
|
6
6
|
module Safrano
|
7
|
+
module RFC2047
|
8
|
+
def self.encode(str)
|
9
|
+
"=?utf-8?b?#{Base64.strict_encode64(str)}?="
|
10
|
+
end
|
11
|
+
end
|
12
|
+
|
7
13
|
module OData
|
8
14
|
# this is used to parse inbound json payload on POST / PUT & co
|
9
15
|
# it does not do symbolize but proper (hopefully) type casting when needed
|
data/lib/safrano/core.rb
CHANGED
@@ -13,10 +13,10 @@ module Safrano
|
|
13
13
|
|
14
14
|
# some prominent constants... probably already defined elsewhere eg in Rack
|
15
15
|
# but lets KISS
|
16
|
-
CONTENT_TYPE = '
|
17
|
-
CONTENT_LENGTH = '
|
18
|
-
LOCATION = '
|
19
|
-
|
16
|
+
CONTENT_TYPE = 'content-type'
|
17
|
+
CONTENT_LENGTH = 'content-length'
|
18
|
+
LOCATION = 'location'
|
19
|
+
|
20
20
|
TEXTPLAIN_UTF8 = 'text/plain;charset=utf-8'
|
21
21
|
APPJSON = 'application/json'
|
22
22
|
APPXML = 'application/xml'
|
data/lib/safrano/multipart.rb
CHANGED
@@ -113,8 +113,8 @@ module MIME
|
|
113
113
|
MPS = 'multipart/'
|
114
114
|
MP_RGX1 = %r{^(digest|mixed);\s*boundary="(.*)"}.freeze
|
115
115
|
MP_RGX2 = %r{^(digest|mixed);\s*boundary=(.*)}.freeze
|
116
|
-
# APP_HTTP_RGX = %r{^application/http}.freeze
|
117
116
|
APP_HTTP = 'application/http'
|
117
|
+
|
118
118
|
def new_content
|
119
119
|
@target =
|
120
120
|
if @target_ct.start_with?(MPS) &&
|
@@ -142,12 +142,12 @@ module MIME
|
|
142
142
|
@lines = inpstr.readlines(@sep)
|
143
143
|
else
|
144
144
|
# rack input wrapper only has gets but not readlines
|
145
|
-
|
146
|
-
|
147
|
-
|
148
|
-
|
149
|
-
|
150
|
-
|
145
|
+
# BUT the rack SPEC says it only supports gets without argument!
|
146
|
+
# --> finally we end up using read and split into lines...
|
147
|
+
# normally should be ok for $batch POST payloads
|
148
|
+
|
149
|
+
# inpstr.read should be a String
|
150
|
+
@lines = inpstr.read.lines(@sep)
|
151
151
|
|
152
152
|
end
|
153
153
|
# tmp hack for test-tools that convert CRLF in payload to LF :-(
|
@@ -319,7 +319,7 @@ module MIME
|
|
319
319
|
def addline(line)
|
320
320
|
@body_lines << line
|
321
321
|
end
|
322
|
-
end
|
322
|
+
end # Parser
|
323
323
|
|
324
324
|
attr_reader :boundary
|
325
325
|
|
@@ -369,12 +369,12 @@ module MIME
|
|
369
369
|
@hd[CTT_TYPE_LC] = "#{Safrano::MP_MIXED}; boundary=#{@boundary}"
|
370
370
|
end
|
371
371
|
|
372
|
-
def
|
373
|
-
get_response(
|
374
|
-
[@response.hd, @response.unparse(true)]
|
372
|
+
def get_mult_resp(full_req)
|
373
|
+
get_response(full_req)
|
374
|
+
[202, @response.hd, @response.unparse(true)]
|
375
375
|
end
|
376
376
|
|
377
|
-
def get_response(
|
377
|
+
def get_response(full_req)
|
378
378
|
@response = self.class.new(::SecureRandom.uuid)
|
379
379
|
@response.set_multipart_header
|
380
380
|
if @level == 1 # changeset need their own global transaction
|
@@ -382,9 +382,10 @@ module MIME
|
|
382
382
|
# and will be flagged with in_changeset=true
|
383
383
|
# and this will finally be used to skip the transaction
|
384
384
|
# of the changes
|
385
|
-
|
385
|
+
|
386
|
+
full_req.db.transaction do
|
386
387
|
begin
|
387
|
-
@response.content = @content.map { |part| part.get_response(
|
388
|
+
@response.content = @content.map { |part| part.get_response(full_req) }
|
388
389
|
rescue Sequel::Rollback => e
|
389
390
|
# one of the changes of the changeset has failed
|
390
391
|
# --> provide a dummy empty response for the change-parts
|
@@ -394,7 +395,7 @@ module MIME
|
|
394
395
|
end
|
395
396
|
end
|
396
397
|
else
|
397
|
-
@response.content = @content.map { |
|
398
|
+
@response.content = @content.map { |part| part.get_response(full_req) }
|
398
399
|
end
|
399
400
|
@response
|
400
401
|
end
|
@@ -450,6 +451,7 @@ module MIME
|
|
450
451
|
class HttpResp < Media
|
451
452
|
attr_accessor :status
|
452
453
|
attr_accessor :content
|
454
|
+
attr_accessor :rack_resp
|
453
455
|
|
454
456
|
APPLICATION_HTTP_11 = ['Content-Type: application/http',
|
455
457
|
"Content-Transfer-Encoding: binary#{CRLF}",
|
@@ -558,9 +560,9 @@ module MIME
|
|
558
560
|
@content = other.content
|
559
561
|
end
|
560
562
|
|
561
|
-
def get_response(
|
562
|
-
# self.content should be the request
|
563
|
-
rack_resp =
|
563
|
+
def get_response(full_req)
|
564
|
+
# self.content should be the part-request
|
565
|
+
rack_resp = full_req.batch_call(@content)
|
564
566
|
@response = MIME::Content::Application::HttpResp.new
|
565
567
|
@response.status = rack_resp[0]
|
566
568
|
@response.hd = rack_resp[1]
|
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,11 @@ 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'
|
16
64
|
# borowed from Sinatra
|
17
65
|
class AcceptEntry
|
18
66
|
attr_accessor :params
|
@@ -87,6 +135,82 @@ module Safrano
|
|
87
135
|
# content-id references map
|
88
136
|
attr_accessor :content_id_references
|
89
137
|
|
138
|
+
include MethodHandlers
|
139
|
+
|
140
|
+
def initialize(env, service_base)
|
141
|
+
super(env)
|
142
|
+
@service_base = service_base
|
143
|
+
end
|
144
|
+
|
145
|
+
# process the request and return finished response
|
146
|
+
def process
|
147
|
+
begin
|
148
|
+
@response = Safrano::Response.new
|
149
|
+
|
150
|
+
before.tap_error { |err| dispatch_error(err) }
|
151
|
+
.tap_valid { |_res| dispatch }
|
152
|
+
|
153
|
+
# handle remaining Sequel errors that we couldnt prevent with our
|
154
|
+
# own pre-checks
|
155
|
+
rescue Sequel::Error => e
|
156
|
+
dispatch_error(SequelExceptionError.new(e))
|
157
|
+
end
|
158
|
+
@response.finish
|
159
|
+
end
|
160
|
+
|
161
|
+
def before
|
162
|
+
negotiate_service_version.tap_valid do
|
163
|
+
myhdrs = NOCACHE_HDRS.dup
|
164
|
+
myhdrs[DATASERVICEVERSION] = @service.data_service_version
|
165
|
+
headers myhdrs
|
166
|
+
end
|
167
|
+
end
|
168
|
+
|
169
|
+
# dispatch for all methods requiring parsing of the path
|
170
|
+
# with walker (ie. allmost all excepted HEAD)
|
171
|
+
def dispatch_with_walker
|
172
|
+
@walker = create_odata_walker
|
173
|
+
case request_method
|
174
|
+
when 'GET'
|
175
|
+
odata_get
|
176
|
+
when 'POST'
|
177
|
+
odata_post
|
178
|
+
when 'DELETE'
|
179
|
+
odata_delete
|
180
|
+
when 'OPTIONS'
|
181
|
+
odata_options
|
182
|
+
when 'PUT'
|
183
|
+
odata_put
|
184
|
+
when 'PATCH', 'MERGE'
|
185
|
+
odata_patch
|
186
|
+
else
|
187
|
+
raise Error
|
188
|
+
end
|
189
|
+
end
|
190
|
+
|
191
|
+
def dispatch_error(err)
|
192
|
+
@response.status, rsph, @response.body = err.odata_get(self)
|
193
|
+
headers rsph
|
194
|
+
end
|
195
|
+
|
196
|
+
def dispatch
|
197
|
+
req_ret = if request_method !~ METHODS_REGEXP
|
198
|
+
[404, EMPTY_HASH, ['Did you get lost?']]
|
199
|
+
elsif request_method == 'HEAD'
|
200
|
+
odata_head
|
201
|
+
else
|
202
|
+
dispatch_with_walker
|
203
|
+
end
|
204
|
+
@response.status, rsph, @response.body = req_ret
|
205
|
+
headers rsph
|
206
|
+
end
|
207
|
+
|
208
|
+
# Set multiple response headers with Hash.
|
209
|
+
def headers(hash = nil)
|
210
|
+
@response.headers.merge! hash if hash
|
211
|
+
@response.headers
|
212
|
+
end
|
213
|
+
|
90
214
|
# stores the newly created entity for the current content-id of
|
91
215
|
# the processed request
|
92
216
|
def register_content_id_ref(new_entity)
|
@@ -213,6 +337,7 @@ module Safrano
|
|
213
337
|
MIN_DTSV_PARSE_ERROR = Safrano::BadRequestError.new(
|
214
338
|
'MinDataServiceVersion could not be parsed'
|
215
339
|
).freeze
|
340
|
+
|
216
341
|
def get_minversion
|
217
342
|
if (rqv = env['HTTP_MINDATASERVICEVERSION'])
|
218
343
|
if (m = DATASERVICEVERSION_RGX.match(rqv))
|
@@ -234,6 +359,7 @@ module Safrano
|
|
234
359
|
MAX_LT_MIN_DTSV_ERROR = Safrano::BadRequestError.new(
|
235
360
|
'MinDataServiceVersion is larger as MaxDataServiceVersion'
|
236
361
|
).freeze
|
362
|
+
|
237
363
|
def negotiate_service_version
|
238
364
|
get_maxversion.if_valid do |maxv|
|
239
365
|
get_minversion.if_valid do |minv|
|
data/lib/safrano/service.rb
CHANGED
@@ -375,7 +375,7 @@ module Safrano
|
|
375
375
|
def finalize_publishing
|
376
376
|
# build the cmap
|
377
377
|
@cmap = {}
|
378
|
-
@collections
|
378
|
+
@collections&.each do |klass|
|
379
379
|
@cmap[klass.entity_set_name] = klass
|
380
380
|
# set namespace needed to have qualified type name
|
381
381
|
copy_namespace_to(klass)
|
@@ -393,7 +393,7 @@ module Safrano
|
|
393
393
|
set_uribase
|
394
394
|
|
395
395
|
# finalize the uri's and include NoMappingBeforeOutput or MappingBeforeOutput as needed
|
396
|
-
@collections
|
396
|
+
@collections&.each do |klass|
|
397
397
|
klass.finalize_publishing(self)
|
398
398
|
|
399
399
|
klass.build_uri(@uribase)
|
@@ -448,9 +448,14 @@ module Safrano
|
|
448
448
|
Safrano::Filter::DateTimeLit.include Safrano::Filter::DateTimeDefault
|
449
449
|
Safrano::Filter::DateTimeOffsetLit.include Safrano::Filter::DateTimeDefault
|
450
450
|
end
|
451
|
+
|
452
|
+
# check function import definition
|
453
|
+
function_imports.each_value { |func| func.check_definition }
|
451
454
|
end
|
452
455
|
|
453
456
|
def execute_deferred_iblocks
|
457
|
+
return unless @collections
|
458
|
+
|
454
459
|
@collections.each do |k|
|
455
460
|
k.instance_eval(&k.deferred_iblock) if k.deferred_iblock
|
456
461
|
end
|
@@ -460,6 +465,8 @@ module Safrano
|
|
460
465
|
## evaluated after '@collections' is filled !
|
461
466
|
# A regexp matching all allowed base entities (eg product|categories )
|
462
467
|
def base_url_regexp
|
468
|
+
return unless @collections
|
469
|
+
|
463
470
|
@collections.map(&:entity_set_name).join('|')
|
464
471
|
end
|
465
472
|
|
data/lib/safrano/version.rb
CHANGED
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.6.
|
4
|
+
version: 0.6.8
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- oz
|
8
8
|
autorequire:
|
9
9
|
bindir: bin
|
10
10
|
cert_chain: []
|
11
|
-
date: 2023-
|
11
|
+
date: 2023-05-13 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
|
-
version: '1.
|
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
|
-
version: '1.
|
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
|
@@ -114,14 +126,14 @@ dependencies:
|
|
114
126
|
requirements:
|
115
127
|
- - "~>"
|
116
128
|
- !ruby/object:Gem::Version
|
117
|
-
version: '
|
129
|
+
version: '2.0'
|
118
130
|
type: :development
|
119
131
|
prerelease: false
|
120
132
|
version_requirements: !ruby/object:Gem::Requirement
|
121
133
|
requirements:
|
122
134
|
- - "~>"
|
123
135
|
- !ruby/object:Gem::Version
|
124
|
-
version: '
|
136
|
+
version: '2.0'
|
125
137
|
- !ruby/object:Gem::Dependency
|
126
138
|
name: rake
|
127
139
|
requirement: !ruby/object:Gem::Requirement
|