safrano 0.6.7 → 0.7.0

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