safrano 0.6.7 → 0.7.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -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