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 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