safrano 0.6.6 → 0.6.8

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: '0108c5a48b2cbd994534ba6732f760dec8f8e8becac690d91b3f1960dd6f7836'
4
- data.tar.gz: c9b8c3c8a7f7ac3d7db5a12133ba3fdcd4815df5b3dab1d35bfaf449518f8d2c
3
+ metadata.gz: 9d1ee63cc31ce50cff784481ce75f5f5581c2b9ff406a18efb516908bbf7c65d
4
+ data.tar.gz: 5770021472ccce8efb445114d4b4834b4fb0ff31b58f6444654cfb16b5aeb860
5
5
  SHA512:
6
- metadata.gz: '0068c08576a0d8df023d6990ad691ce73b1d3abd6f3a9cd095b7ae2f9358dc7d33d7ed5d0fdde2503a2829ebeb11ab126961a39d34d12370befbb5814904f7b1'
7
- data.tar.gz: cbf6fbdd2d1f4a24ba58b14490dfcfc87e1ffc333ab52b0e334b06742679543a794359f16093f56cc78e7a754724ee3e24f70ec7b7277d3ce6d04af3d1f9ae5f
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
- # Mayonaise
25
- class MyOApp < Safrano::ServerApp
26
- attr_reader :full_req
27
- attr_reader :response
28
- attr_reader :db
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
- @db = full_req.service.collections.first.db
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
- @request.service = @full_req.service
39
- headers 'DataServiceVersion' => @request.service.data_service_version
47
+ @service = @full_req.service
48
+ headers 'DataServiceVersion' => @service.data_service_version
40
49
  end
41
50
 
42
- def batch_call(part_req)
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
- @request = Safrano::Request.new(env)
53
+
47
54
  @response = Safrano::Response.new
48
55
 
49
- if part_req.level == 2
50
- @request.in_changeset = true
51
- @request.content_id = part_req.content_id
52
- @request.content_id_references = part_req.content_id_references
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] = @full_req.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
- # Huile d'olive extra
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
- # jaune d'oeuf
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
- # battre le tout
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
- resp_hdrs, @mult_response.body = @mult_request.get_http_resp(batcha)
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
- @file_server = ::Rack::File.new(@root)
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 = File.absolute_path(@media_dir_name, @root)
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: @file_server works relative to @root directory
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
- fsret = @file_server.call(media_env)
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
- File.join(@media_dir_name, media_directory(entity))
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
- Dir.chdir(abs_path(entity)) do
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
- File.absolute_path(media_path(entity), @root)
94
+ @root + media_path(entity)
75
95
  end
76
96
 
77
97
  # absolute filename
78
98
  def abs_filename(entity)
79
- File.absolute_path(filename(entity), @root)
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
- def in_media_directory(entity)
90
- mpi = media_directory(entity)
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
- Dir.chdir mpi do
93
- yield
94
- end
119
+ yield Pathname(mpi)
95
120
  end
96
121
 
97
122
  def odata_delete(entity:)
98
- Dir.chdir(@abs_klass_dir) do
99
- in_media_directory(entity) do
100
- Dir.glob('*').each { |oldf| File.delete(oldf) }
101
- end
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
- Dir.chdir(@abs_klass_dir) do
108
- in_media_directory(entity) do
109
- filename = '1'
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
- Dir.chdir(@abs_klass_dir) do
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
- # Here as well, MVP implementation
127
- def replace_file(data:, filename:, entity:)
128
- Dir.chdir(@abs_klass_dir) do
129
- in_media_directory(entity) do
130
- version = nil
131
- Dir.glob('*').sort.each do |oldf|
132
- version = oldf
133
- File.delete(oldf)
134
- end
135
- filename = (version.to_i + 1).to_s
136
- File.open(filename, 'wb') { |f| IO.copy_stream(data, f) }
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
- def in_media_directory(entity)
166
- mpi = media_directory(entity)
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
- Dir.chdir(mpi) { yield }
205
+ yield Pathname(mpi)
169
206
  end
170
207
 
171
208
  def odata_delete(entity:)
172
- Dir.chdir(@abs_klass_dir) do
173
- in_media_directory(entity) do
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
- # Here as well, MVP implementation
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
@@ -372,8 +372,7 @@ module Safrano
372
372
 
373
373
  end
374
374
  model.media_handler.replace_file(data: data,
375
- entity: self,
376
- filename: filename)
375
+ entity: self)
377
376
 
378
377
  ARY_204_EMPTY_HASH_ARY
379
378
  end
@@ -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
- @proc = proc
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 return_collection(klassmod, &proc)
75
- raise('Please provide a code block') unless block_given?
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
- @proc = proc
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
- return yield unless (@error = check_url_func_params)
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
 
@@ -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
- if primary_key.is_a? Symbol # single key field
449
- @pk_castfunc = @casted_cols[primary_key]
450
- end
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, mrgx = scan_rgx_parts.find do |_pk, rgx|
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, { 'Location' => new_entity.uri }, new_entity.to_odata_create_json(request: req)]
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
@@ -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 = 'Content-Type'
17
- CONTENT_LENGTH = 'Content-Length'
18
- LOCATION = 'Location'
19
- CTT_TYPE_LC = 'content-type'
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'
@@ -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
- sepsave = $INPUT_RECORD_SEPARATOR
146
- $INPUT_RECORD_SEPARATOR = @sep
147
- while (line = inpstr.gets)
148
- @lines << line
149
- end
150
- $INPUT_RECORD_SEPARATOR = sepsave
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 get_http_resp(batcha)
373
- get_response(batcha)
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(batcha)
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
- batcha.db.transaction do
385
+
386
+ full_req.db.transaction do
386
387
  begin
387
- @response.content = @content.map { |part| part.get_response(batcha) }
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 { |prt| prt.get_response(batcha) }
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(batchapp)
562
- # self.content should be the request
563
- rack_resp = batchapp.batch_call(@content)
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]
@@ -6,143 +6,33 @@ require_relative 'request'
6
6
  require_relative 'response'
7
7
 
8
8
  module Safrano
9
- # handle GET PUT etc
10
- module MethodHandlers
11
- def odata_options
12
- @walker.finalize.tap_error { |err| return err.odata_get(@request) }
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
- METHODS_REGEXP = Regexp.new('HEAD|OPTIONS|GET|POST|PATCH|MERGE|PUT|DELETE').freeze
56
- NOCACHE_HDRS = { 'Cache-Control' => 'no-cache',
57
- 'Expires' => '-1',
58
- 'Pragma' => 'no-cache' }.freeze
59
- DATASERVICEVERSION = 'DataServiceVersion'
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
- # for thread safety
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
@@ -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|
@@ -375,7 +375,7 @@ module Safrano
375
375
  def finalize_publishing
376
376
  # build the cmap
377
377
  @cmap = {}
378
- @collections.each do |klass|
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.each do |klass|
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
 
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Safrano
4
- VERSION = '0.6.6'
4
+ VERSION = '0.6.8'
5
5
  end
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.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-01-04 00:00:00.000000000 Z
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.0'
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.0'
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: '1.0'
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: '1.0'
136
+ version: '2.0'
125
137
  - !ruby/object:Gem::Dependency
126
138
  name: rake
127
139
  requirement: !ruby/object:Gem::Requirement