safrano 0.4.5 → 0.5.3

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: 9f2ebcbe8cf374526aa263f93515e2424753898ad878904d6d5d087bc6317c4e
4
- data.tar.gz: 48190a71451991630bd48347c3cf2acb701423bb58266fa736b0cf7a5b6f3bdf
3
+ metadata.gz: 64699787371f5e9f696461faac80382cf2d53fe78d387742d8650e5a4b2c54df
4
+ data.tar.gz: 530e7619b4b27a1a50a63f832d4f3a8894850c6cea36e638769cdffbc6b7d942
5
5
  SHA512:
6
- metadata.gz: 70ef7d275b566269879e4cd37f019958cd354504d1dede907adcf4d078e47361412997c69eec2c403317013ef5ac8019315aeb8cc89715906f2f12bd59e885dc
7
- data.tar.gz: 1ff899294284a5e9531187533938e14ae304c1344131177dff1612037ca32e6a27dfcd72b367dd705afbb5cd5b9d38e9c11dc7b8f7766d470c2ac4c45686372b
6
+ metadata.gz: e5d118f15a3a3ba78f5385867c82370e36612585b7f83329343babb2e3a8a7220d1059ad766fbe7c9e1f7f74a57945c086e60624d0301259c1427f0053b59052
7
+ data.tar.gz: 52b283be3e8902226f14864a74eae30bc62c65908be748ea37c5a150f574f3130c8957f26251e1958e298eba221eeada9f0155fcc73ed2591034e97466f1e629
@@ -60,10 +60,13 @@ module Safrano
60
60
  end
61
61
 
62
62
  def initialize_dataset(dtset = nil)
63
- @cx = dtset || @modelk
63
+ @cx = @cx || dtset || @modelk
64
+ end
65
+
66
+ def initialize_uparms
64
67
  @uparms = UrlParameters4Coll.new(@cx, @params)
65
68
  end
66
-
69
+
67
70
  def odata_get_apply_params
68
71
  @uparms.apply_to_dataset(@cx).map_result! do |dataset|
69
72
  @cx = dataset
@@ -131,9 +134,9 @@ module Safrano
131
134
 
132
135
  # on model class level we return the collection
133
136
  def odata_get(req)
134
- @params = req.params
135
- initialize_dataset
136
-
137
+ @params = @params || req.params
138
+ initialize_dataset
139
+ initialize_uparms
137
140
  @uparms.check_all.if_valid { |_ret|
138
141
  odata_get_apply_params.if_valid { |_ret|
139
142
  odata_get_output(req)
@@ -170,8 +173,7 @@ module Safrano
170
173
  end
171
174
 
172
175
  def initialize_dataset(dtset = nil)
173
- @cx = dtset || navigated_dataset
174
- @uparms = UrlParameters4Coll.new(@cx, @params)
176
+ @cx = @cx || dtset || navigated_dataset
175
177
  end
176
178
  # redefinitions of the main methods for a navigated collection
177
179
  # (eg. all Books of Author[2] is Author[2].Books.all )
@@ -45,12 +45,12 @@ module Safrano
45
45
  # the join-helper is shared by the order-by object and was potentially already
46
46
  # partly built on order-by object creation.
47
47
  def finalize(jh)
48
- @filtexpr = @ast.if_valid { |ast| ast.sequel_expr(jh) }
48
+ @filtexpr = @ast.if_valid { |ast| ast.sequel_expr(jh) }
49
49
  end
50
50
 
51
51
  def apply_to_dataset(dtcx)
52
52
  # normally finalize is called before, and thus @filtexpr is set
53
- @filtexpr.map_result! { |f| dtcx.where(f) }
53
+ @filtexpr.map_result! { |f| dtcx.where(f) }
54
54
  end
55
55
 
56
56
  # Note: this is really only *parse* error, ie the error encounterd while
@@ -8,6 +8,11 @@ module Safrano
8
8
  module Media
9
9
  # base class for Media Handler
10
10
  class Handler
11
+ def check_before_create(data:,
12
+ entity:,
13
+ filename:)
14
+ Contract::OK
15
+ end
11
16
  end
12
17
 
13
18
  # Simple static File/Directory based media store handler
@@ -54,13 +59,13 @@ module Safrano
54
59
  end
55
60
 
56
61
  # relative to @root
57
- # eg Photo/1/pommes-topaz.jpg
62
+ # eg Photo/1/1
58
63
  def filename(entity)
59
64
  Dir.chdir(abs_path(entity)) do
60
65
  # simple design: one file per directory, and the directory
61
66
  # contains the media entity-id --> implicit link between the media
62
67
  # entity
63
- File.join(media_path(entity), Dir.glob('*').min)
68
+ File.join(media_path(entity), Dir.glob('*').max)
64
69
  end
65
70
  end
66
71
 
@@ -147,15 +152,14 @@ module Safrano
147
152
 
148
153
  # this is relative to abs_klass_dir(entity) eg to /@root/Photo
149
154
  # tree-structure
150
- # media_path_ids = 1 --> 1
151
- # media_path_ids = 15 --> 1/5
152
- # media_path_ids = 555 --> 5/5/5
153
- # media_path_ids = 5,5,5 --> 5/00/5/00/5
154
- # media_path_ids = 5,00,5 --> 5/00/0/0/00/5
155
- # media_path_ids = 5,xyz,5 --> 5/00/x/y/z/00/5
155
+ # media_path_ids = 1 --> 1/v
156
+ # media_path_ids = 15 --> 1/5/v
157
+ # media_path_ids = 555 --> 5/5/5/v
158
+ # media_path_ids = 5,5,5 --> 5/00/5/00/5/v
159
+ # media_path_ids = 5,00,5 --> 5/00/0/0/00/5/v
160
+ # media_path_ids = 5,xyz,5 --> 5/00/x/y/z/00/5/v
156
161
  def media_directory(entity)
157
162
  StaticTree.path_builder(entity.media_path_ids)
158
- # entity.media_path_ids.map{|id| id.to_s.chars.join('/')}.join(@sep)
159
163
  end
160
164
 
161
165
  def in_media_directory(entity)
@@ -267,29 +271,39 @@ module Safrano
267
271
  missing: :skip)
268
272
  end
269
273
 
270
- # to_one rels are create with FK data set on the parent entity
271
- if parent
272
- odata_create_save_entity_and_rel(req, new_entity, assoc, parent)
273
- else
274
- # in-changeset requests get their own transaction
275
- new_entity.save(transaction: !req.in_changeset)
276
- end
274
+ # call before_create_entity media hook
275
+ new_entity.before_create_media_entity(data: data, mimetype: mimetype) if new_entity.respond_to? :before_create_media_entity
276
+
277
+ media_handler.check_before_create(data: data,
278
+ entity: new_entity,
279
+ filename: filename).if_valid { |_ret|
280
+ # to_one rels are create with FK data set on the parent entity
281
+ if parent
282
+ odata_create_save_entity_and_rel(req, new_entity, assoc, parent)
283
+ else
284
+ # in-changeset requests get their own transaction
285
+ new_entity.save(transaction: !req.in_changeset)
286
+ end
287
+
288
+ req.register_content_id_ref(new_entity)
289
+ new_entity.copy_request_infos(req)
277
290
 
278
- req.register_content_id_ref(new_entity)
279
- new_entity.copy_request_infos(req)
291
+ # call before_create_media hook
292
+ new_entity.before_create_media if new_entity.respond_to? :before_create_media
280
293
 
281
- # call before_create_media hook
282
- new_entity.before_create_media if new_entity.respond_to? :before_create_media
294
+ media_handler.save_file(data: data,
295
+ entity: new_entity,
296
+ filename: filename)
283
297
 
284
- media_handler.save_file(data: data,
285
- entity: new_entity,
286
- filename: filename)
298
+ # call after_create_media hook
299
+ new_entity.after_create_media if new_entity.respond_to? :after_create_media
287
300
 
288
- # call after_create_media hook
289
- new_entity.after_create_media if new_entity.respond_to? :after_create_media
301
+ # json is default content type so we dont need to specify it here again
302
+ # Contract.valid([201, EMPTY_HASH, new_entity.to_odata_post_json(service: req.service)])
303
+ # TODO quirks array mode !
304
+ Contract.valid([201, EMPTY_HASH, new_entity.to_odata_create_json(request: req)])
305
+ }.tap_error { |e| return e.odata_get(req) }.result
290
306
 
291
- # json is default content type so we dont need to specify it here again
292
- [201, EMPTY_HASH, new_entity.to_odata_post_json(service: req.service)]
293
307
  else # TODO: other formats
294
308
  415
295
309
  end
@@ -2,6 +2,7 @@
2
2
 
3
3
  module Safrano
4
4
  module FunctionImport
5
+ EMPTY_HASH = {}.freeze
5
6
  class ResultDefinition
6
7
  D = 'd'
7
8
  DJ_OPEN = '{"d":'
@@ -11,67 +12,174 @@ module Safrano
11
12
  VALUEK = 'value'
12
13
  RESULTSK = 'results'
13
14
  COLLECTION = 'Collection'
14
-
15
- def initialize(klassmod)
16
- @klassmod = klassmod
15
+
16
+ def allowed_transitions
17
+ [Safrano::TransitionEnd]
17
18
  end
18
-
19
- def to_odata_json(result, _req)
20
- "#{DJ_OPEN}#{result.odata_h.to_json}#{DJ_CLOSE}"
19
+
20
+ def transition_end(_match_result)
21
+ Safrano::Transition::RESULT_END
21
22
  end
22
-
23
- def type_metadata
23
+
24
+ # we will have this on class and instance level for making things simpler first
25
+ def self.klassmod
26
+ @klassmod
27
+ end
28
+
29
+ # return a subclass of ResultAsComplexType
30
+ def self.asComplexType(klassmod)
31
+ Class.new(ResultAsComplexType) do
32
+ @klassmod = klassmod
33
+ end
34
+ end
35
+
36
+ # return a subclass of ResultAsComplexType
37
+ def self.asComplexTypeColl(klassmod)
38
+ Class.new(ResultAsComplexTypeColl) do
39
+ @klassmod = klassmod
40
+ end
41
+ end
42
+
43
+ def self.asPrimitiveType(klassmod)
44
+ Class.new(ResultAsPrimitiveType) do
45
+ @klassmod = klassmod
46
+ end
47
+ end
48
+
49
+ def self.asPrimitiveTypeColl(klassmod)
50
+ Class.new(ResultAsPrimitiveTypeColl) do
51
+ @klassmod = klassmod
52
+ end
53
+ end
54
+
55
+ def self.asEntity(klassmod)
56
+ Class.new(ResultAsEntity) do
57
+ @klassmod = klassmod
58
+ end
59
+ end
60
+
61
+ def self.asEntityColl(klassmod)
62
+ Class.new(ResultAsEntityColl) do
63
+ @klassmod = klassmod
64
+ end
65
+ end
66
+
67
+ def initialize(value)
68
+ @value = value
69
+ end
70
+
71
+ def odata_get(req)
72
+ [200, EMPTY_HASH, [to_odata_json(req)]]
73
+ end
74
+ def self.type_metadata
24
75
  @klassmod.type_name
25
76
  end
77
+ def type_metadata
78
+ self.class.type_metadata
79
+ end
80
+
81
+ # needed for ComplexType result
82
+ def to_odata_json(_req)
83
+ "#{DJ_OPEN}#{@value.odata_h.to_json}#{DJ_CLOSE}"
84
+ end
85
+
86
+ # wrapper
87
+ # for OData Entity and Collections, return them directly
88
+ # for others, ie ComplexType, Prims etc, return the ResultDefinition-subclass wrapped result
89
+ def self.do_execute_func_result(result, _req, apply_query_params=false)
90
+ self.new(result)
91
+ end
92
+
26
93
  end
94
+
27
95
  class ResultAsComplexType < ResultDefinition
96
+ def self.type_metadata
97
+ @klassmod.type_name
98
+ end
28
99
  end
100
+
29
101
  class ResultAsComplexTypeColl < ResultDefinition
30
- def type_metadata
102
+ def self.type_metadata
31
103
  "Collection(#{@klassmod.type_name})"
32
104
  end
33
105
 
34
- def to_odata_json(coll, _req)
35
- "#{DJ_OPEN}#{{ RESULTSK => coll.map { |c| c.odata_h } }.to_json}#{DJ_CLOSE}"
106
+ def to_odata_json(req)
107
+ # "#{DJ_OPEN}#{{ RESULTSK => coll.map { |c| c.odata_h } }.to_json}#{DJ_CLOSE}"
108
+ template = self.class.klassmod.output_template
109
+ # TODO: Error handling if database contains binary BLOB data that cant be
110
+ # interpreted as UTF-8 then JSON will fail here
111
+
112
+ innerh = req.service.get_coll_odata_h(array: @value,
113
+ template: template)
114
+
115
+ innerj = innerh.to_json
116
+
117
+ "#{DJ_OPEN}#{innerj}#{DJ_CLOSE}"
36
118
  end
37
119
  end
120
+
38
121
  class ResultAsEntity < ResultDefinition
39
- def to_odata_json(result_entity, req)
40
- result_entity.instance_exec do
41
- copy_request_infos(req)
42
- to_odata_json(request: req)
43
- end
122
+
123
+ def self.type_metadata
124
+ @klassmod.type_name
44
125
  end
126
+
127
+
128
+ # wrapper
129
+ # for OData Entity return them directly
130
+ def self.do_execute_func_result(result, _req, apply_query_params=false)
131
+ # note: Sequel entities instances seem to be thread safe, so we can
132
+ # safely add request-dependant data (eg. req.params) there
133
+ apply_query_params ? result : result.inactive_query_params
134
+ end
135
+
45
136
  end
137
+
46
138
  class ResultAsEntityColl < ResultDefinition
47
- def type_metadata
139
+
140
+ def self.type_metadata
48
141
  "Collection(#{@klassmod.type_name})"
49
142
  end
50
-
51
- def to_odata_json(result_dataset, req)
143
+
144
+ # wrapper
145
+ # for OData Entity Collection return them directly
146
+ def self.do_execute_func_result(result, req, apply_query_params=false)
52
147
  coll = Safrano::OData::Collection.new(@klassmod)
148
+ # instance_exec has other instance variables; @values would be nil in the block below
149
+ # need to pass a local copy
150
+ dtset = result
53
151
  coll.instance_exec do
54
- @params = req.params
55
- initialize_dataset(result_dataset)
152
+
153
+ @params = apply_query_params ? req.params : EMPTY_HASH
154
+ initialize_dataset(dtset)
155
+ initialize_uparms
56
156
  end
57
- coll.to_odata_json(request: req)
157
+ coll
58
158
  end
159
+
59
160
  end
161
+
60
162
  class ResultAsPrimitiveType < ResultDefinition
61
- def to_odata_json(result, _req)
163
+ def self.type_metadata
164
+ @klassmod.type_name
165
+ end
166
+
167
+ def to_odata_json(_req)
62
168
  { D => { METAK => { TYPEK => type_metadata },
63
- VALUEK => @klassmod.odata_value(result) } }.to_json
169
+ VALUEK => self.class.klassmod.odata_value(@value) } }.to_json
64
170
  end
65
171
  end
172
+
66
173
  class ResultAsPrimitiveTypeColl < ResultDefinition
67
- def type_metadata
174
+ def self.type_metadata
68
175
  "Collection(#{@klassmod.type_name})"
69
176
  end
70
177
 
71
- def to_odata_json(result, _req)
72
- { D => { METAK => { TYPEK => type_metadata },
73
- RESULTSK => @klassmod.odata_collection(result) } }.to_json
178
+ def to_odata_json(_req)
179
+ { D => { METAK => { TYPEK => self.class.type_metadata },
180
+ RESULTSK => self.class.klassmod.odata_collection(@value) } }.to_json
74
181
  end
182
+
75
183
  end
76
184
  end
77
185
 
@@ -80,7 +188,8 @@ module Safrano
80
188
  # with added OData functionality
81
189
  class ComplexType
82
190
  attr_reader :values
83
-
191
+ EMPTYH = {}.freeze
192
+
84
193
  @namespace = nil
85
194
  def self.namespace
86
195
  @namespace
@@ -89,9 +198,41 @@ module Safrano
89
198
  def self.props
90
199
  @props
91
200
  end
92
-
201
+
202
+ def type_name
203
+ self.class.type_name
204
+ end
205
+
206
+ def metadata_h
207
+ { type: type_name }
208
+ end
209
+
210
+ def casted_values
211
+ # MVP... TODO: handle time mappings like in Entity models
212
+ values
213
+ end
214
+
215
+ # needed for nested json output
216
+ # this is a simpler version of model_ext#output_template
217
+ def self.default_template
218
+ template = {}
219
+ expand_e = {}
220
+
221
+ template[:all_values] = EMPTYH
222
+ @props.each { |prop, kl|
223
+ if kl.respond_to? :default_template
224
+ expand_e[prop] = kl.default_template
225
+ end
226
+ }
227
+ template[:expand_e] = expand_e
228
+ template
229
+ end
230
+
231
+ def self.output_template
232
+ default_template
233
+ end
93
234
  def self.type_name
94
- "#{@namespace}.#{self.to_s}"
235
+ @namespace ? "#{@namespace}.#{self.to_s}" : self.to_s
95
236
  end
96
237
 
97
238
  def initialize
@@ -102,6 +243,7 @@ module Safrano
102
243
 
103
244
  def odata_h
104
245
  ret = { METAK => { TYPEK => self.class.type_name } }
246
+
105
247
  @values.each { |k, v|
106
248
  ret[k] = if v.respond_to? :odata_h
107
249
  v.odata_h
@@ -113,11 +255,11 @@ module Safrano
113
255
  end
114
256
 
115
257
  def self.return_as_collection_descriptor
116
- FunctionImport::ResultAsComplexTypeColl.new(self)
258
+ FunctionImport::ResultDefinition.asComplexTypeColl(self)
117
259
  end
118
260
 
119
261
  def self.return_as_instance_descriptor
120
- FunctionImport::ResultAsComplexType.new(self)
262
+ FunctionImport::ResultDefinition.asComplexType(self)
121
263
  end
122
264
 
123
265
  # add metadata xml to the passed REXML schema object
@@ -20,13 +20,15 @@ module Safrano
20
20
  # cf. Sequel Database column_schema_default_to_ruby_value
21
21
  # schema_column_type
22
22
  # https://www.odata.org/documentation/odata-version-2-0/overview/
23
- def self.default_edm_type(ruby_type:)
23
+ def self.default_edm_type(ruby_type:, db_type: )
24
24
  case ruby_type
25
25
  when :integer
26
26
  'Edm.Int32'
27
27
  when :string
28
28
  'Edm.String'
29
- when :date, :datetime,
29
+ when :date
30
+ 'Edm.DateTime'
31
+ when :datetime
30
32
  'Edm.DateTime'
31
33
  when :time
32
34
  'Edm.Time'
@@ -38,6 +40,10 @@ module Safrano
38
40
  'Edm.Decimal'
39
41
  when :blob
40
42
  'Edm.Binary'
43
+ else # try with db_type:
44
+ if ( db_type =~ /\ANUMERIC/ )
45
+ 'Edm.Decimal'
46
+ end
41
47
  end
42
48
  end
43
49
 
data/lib/odata/entity.rb CHANGED
@@ -112,10 +112,11 @@ module Safrano
112
112
  selvals
113
113
  end
114
114
 
115
- # post paylod expects the new entity in an array
116
- def to_odata_post_json(service:)
117
- innerj = service.get_coll_odata_h(array: [self],
118
- template: self.class.default_template).to_json
115
+ # some clients wrongly expect post payload with the new entity in an array
116
+ # TODO quirks array mode !
117
+ def to_odata_array_json(request:)
118
+ innerj = request.service.get_coll_odata_h(array: [self],
119
+ template: self.class.default_template).to_json
119
120
  "#{DJ_OPEN}#{innerj}#{DJ_CLOSE}"
120
121
  end
121
122
 
@@ -124,7 +125,7 @@ module Safrano
124
125
  end
125
126
 
126
127
  def copy_request_infos(req)
127
- @params = req.params
128
+ @params = @inactive_query_params ? EMPTY_HASH : req.params
128
129
  @do_links = req.walker.do_links
129
130
  @uparms = UrlParameters4Single.new(self, @params)
130
131
  end
@@ -150,7 +151,11 @@ module Safrano
150
151
  @uparms.check_all.tap_valid { return odata_get_output(req) }
151
152
  .tap_error { |e| return e.odata_get(req) }
152
153
  end
153
-
154
+ def inactive_query_params
155
+ @inactive_query_params = true
156
+ self # chaining
157
+ end
158
+
154
159
  DELETE_REL_AND_ENTY = lambda do |entity, assoc, parent|
155
160
  Safrano.remove_nav_relation(assoc, parent)
156
161
  entity.destroy(transaction: false)
@@ -263,6 +268,7 @@ module Safrano
263
268
  ret
264
269
  end
265
270
  end
271
+
266
272
  # end of module SafranoEntity
267
273
  module Entity
268
274
  include EntityBase
@@ -305,7 +311,7 @@ module Safrano
305
311
  end
306
312
 
307
313
  module MappingBeforeOutput
308
- # needed for proper datetime output
314
+ # needed for proper datetime or Decimal output
309
315
  def casted_values(cols = nil)
310
316
  vals = case cols
311
317
  when nil
@@ -316,6 +322,10 @@ module Safrano
316
322
  else
317
323
  selected_values_for_odata(cols)
318
324
  end
325
+ # TODO better design (perf/ do more during startup and less during request runtime )
326
+ # TODO replace the quick and dirty BigDecimal hack with something better
327
+ self.class.decimal_cols.each { |dc| vals[dc] = BigDecimal(vals[dc].to_s).to_s('F') if vals.key?(dc) }
328
+
319
329
  self.class.time_cols.each { |tc| vals[tc] = vals[tc]&.iso8601 if vals.key?(tc) }
320
330
  vals
321
331
  end
@@ -438,5 +448,22 @@ module Safrano
438
448
  pk_hash.values
439
449
  end
440
450
  end
441
- end
442
- # end of Module OData
451
+
452
+ module EntityCreateStandardOutput
453
+ # Json formatter for a create entity POST call / Standard version; return as json object
454
+ def to_odata_create_json(request:)
455
+ # TODO Perf: reduce method call overhead
456
+ # we added this redirection for readability and flexibility
457
+ to_odata_json(request: request)
458
+ end
459
+ end
460
+
461
+ module EntityCreateArrayOutput
462
+ # Json formatter for a create entity POST call Array version
463
+ def to_odata_create_json(request:)
464
+ # TODO Perf: reduce method call overhead
465
+ # we added this redirection for readability and flexibility
466
+ to_odata_array_json(request: request)
467
+ end
468
+ end
469
+ end # end of Module OData
data/lib/odata/error.rb CHANGED
@@ -114,6 +114,18 @@ module Safrano
114
114
  end
115
115
  end
116
116
 
117
+ # http Unprocessable Entity for example when trying to
118
+ # upload duplicated media ressource
119
+ class UnprocessableEntityError
120
+ extend ErrorClass
121
+ include ErrorInstance
122
+ HTTP_CODE = 422
123
+ @msg = 'Unprocessable Entity'
124
+ def initialize(reason)
125
+ @msg = reason
126
+ end
127
+ end
128
+
117
129
  # http Bad Req.
118
130
  class BadRequestError
119
131
  extend ErrorClass
@@ -125,6 +137,13 @@ module Safrano
125
137
  end
126
138
  end
127
139
 
140
+ # for upload empty media
141
+ class BadRequestEmptyMediaUpload < BadRequestError
142
+ include ErrorInstance
143
+ def initialize(path)
144
+ @msg = "Bad Request: empty media file #{path}"
145
+ end
146
+ end
128
147
  # Generic failed changeset
129
148
  class BadRequestFailedChangeSet < BadRequestError
130
149
  @msg = 'Bad Request: Failed changeset '
@@ -21,7 +21,7 @@ module Safrano
21
21
  end
22
22
 
23
23
  def allowed_transitions
24
- [Safrano::TransitionEnd]
24
+ [Safrano::TransitionExecuteFunc]
25
25
  end
26
26
 
27
27
  def input(**parmtypes)
@@ -42,7 +42,13 @@ module Safrano
42
42
  end
43
43
  self
44
44
  end
45
-
45
+
46
+ def auto_query_parameters
47
+ @auto_query_params = true
48
+ self # chaining
49
+ end
50
+ alias auto_query_params auto_query_parameters
51
+
46
52
  def return(klassmod, &proc)
47
53
  raise('Please provide a code block') unless block_given?
48
54
 
@@ -51,7 +57,7 @@ module Safrano
51
57
  else
52
58
  # if it's neither a ComplexType nor a Model-Entity
53
59
  # --> assume it is a Primitive
54
- ResultAsPrimitiveType.new(klassmod)
60
+ ResultDefinition.asPrimitiveType(klassmod)
55
61
  end
56
62
  @proc = proc
57
63
  self
@@ -65,7 +71,8 @@ module Safrano
65
71
  else
66
72
  # if it's neither a ComplexType nor a Modle-Entity
67
73
  # --> assume it is a Primitive
68
- ResultAsPrimitiveTypeColl.new(klassmod)
74
+ # ResultAsPrimitiveTypeColl.new(klassmod)
75
+ ResultDefinition.asPrimitiveTypeColl(klassmod)
69
76
  end
70
77
  @proc = proc
71
78
  self
@@ -135,33 +142,25 @@ module Safrano
135
142
  end if @input
136
143
  funky
137
144
  end
138
-
139
- def with_validated_get(req)
145
+
146
+ def with_transition_validated(req)
140
147
  # initialize_params
148
+ @params = req.params
141
149
  return yield unless (@error = check_url_func_params)
142
150
 
143
- @error.odata_get(req) if @error
144
- end
145
-
146
- def to_odata_json(req)
147
- result = @proc.call(**@funcparams)
148
- @returning.to_odata_json(result, req)
151
+ [nil, :error, @error] if @error
149
152
  end
150
-
151
- def odata_get_output(req)
152
- [200, EMPTY_HASH, [to_odata_json(req)]]
153
- end
154
-
155
- def odata_get(req)
156
- @params = req.params
157
-
158
- with_validated_get(req) do
159
- odata_get_output(req)
153
+
154
+
155
+ def do_execute_func(req)
156
+ with_transition_validated(req) do
157
+ result = @proc.call(**@funcparams)
158
+ [@returning.do_execute_func_result(result, req, @auto_query_params), :run]
160
159
  end
161
160
  end
162
-
163
- def transition_end(_match_result)
164
- Transition::RESULT_END
161
+
162
+ def transition_execute_func(_match_result)
163
+ [self, :run_with_execute_func]
165
164
  end
166
165
  end
167
166
  end
@@ -6,6 +6,7 @@
6
6
 
7
7
  require 'json'
8
8
  require 'rexml/document'
9
+ require 'bigdecimal'
9
10
  require_relative '../safrano/core'
10
11
  require_relative 'error'
11
12
  require_relative 'collection_filter'
@@ -32,6 +33,7 @@ module Safrano
32
33
  attr_reader :uri
33
34
  attr_reader :odata_upk_parts
34
35
  attr_reader :time_cols
36
+ attr_reader :decimal_cols
35
37
  attr_reader :namespace
36
38
 
37
39
  # initialising block of code to be executed at end of
@@ -73,7 +75,7 @@ module Safrano
73
75
  @uparms = nil
74
76
  @params = nil
75
77
  @cx = nil
76
- @@time_cols = nil
78
+ # @@time_cols = nil
77
79
  end
78
80
 
79
81
  def build_uri(uribase)
@@ -81,11 +83,11 @@ module Safrano
81
83
  end
82
84
 
83
85
  def return_as_collection_descriptor
84
- Safrano::FunctionImport::ResultAsEntityColl.new(self)
86
+ Safrano::FunctionImport::ResultDefinition.asEntityColl(self)
85
87
  end
86
88
 
87
89
  def return_as_instance_descriptor
88
- Safrano::FunctionImport::ResultAsEntity.new(self)
90
+ Safrano::FunctionImport::ResultDefinition.asEntity(self)
89
91
  end
90
92
 
91
93
  def execute_deferred_iblock
@@ -94,8 +96,10 @@ module Safrano
94
96
 
95
97
  # Factory json-> Model Object instance
96
98
  def new_from_hson_h(hash)
97
- enty = new
98
- enty.set_fields(hash, data_fields, missing: :skip)
99
+ #enty = new
100
+ #enty.set_fields(hash, data_fields, missing: :skip)
101
+ enty = create(hash)
102
+ #enty.set(hash)
99
103
  enty
100
104
  end
101
105
 
@@ -356,11 +360,16 @@ module Safrano
356
360
  # Time columns
357
361
  @time_cols = db_schema.select { |_c, v| v[:type] == :datetime }.map { |c, _v| c }
358
362
 
363
+
364
+
359
365
  # add edm_types into schema
360
366
  db_schema.each do |_col, props|
361
- props[:odata_edm_type] = Safrano.default_edm_type(ruby_type: props[:type])
367
+ props[:odata_edm_type] = Safrano.default_edm_type(ruby_type: props[:type],
368
+ db_type: props[:db_type])
362
369
  end
363
-
370
+ # Edm.Decimal cols
371
+ @decimal_cols = db_schema.select { |_c, v| v[:odata_edm_type] == 'Edm.Decimal' }.map { |c, _v| c }
372
+
364
373
  # and finally build the path lists and allowed tr's
365
374
  build_attribute_path_list
366
375
  build_expand_path_list
@@ -627,7 +636,9 @@ module Safrano
627
636
  req.register_content_id_ref(new_entity)
628
637
  new_entity.copy_request_infos(req)
629
638
  # json is default content type so we dont need to specify it here again
630
- [201, EMPTY_HASH, new_entity.to_odata_post_json(service: req.service)]
639
+ # TODO quirks array mode !
640
+ # [201, EMPTY_HASH, new_entity.to_odata_post_json(service: req.service)]
641
+ [201, EMPTY_HASH, new_entity.to_odata_create_json(request: req)]
631
642
  else # TODO: other formats
632
643
  415
633
644
  end
@@ -6,7 +6,7 @@ require_relative 'error'
6
6
  module Safrano
7
7
  # represents a state transition when navigating/parsing the url path
8
8
  # from left to right
9
- class Transition < Regexp
9
+ class Transition
10
10
  attr_accessor :trans
11
11
  attr_accessor :match_result
12
12
  attr_accessor :rgx
@@ -52,8 +52,28 @@ module Safrano
52
52
  ctx.method(@trans).call(@match_result)
53
53
  end
54
54
  end
55
-
55
+
56
+ #Transition that does not move/change the input
57
+ class InplaceTransition < Transition
58
+ def initialize(trans: )
59
+ @trans = trans
60
+ end
61
+ def do_match(str)
62
+ @str = str
63
+ end
64
+ def path_remain
65
+ @str
66
+ end
67
+ def path_done
68
+ EMPTYSTR
69
+ end
70
+ def do_transition(ctx)
71
+ ctx.method(@trans).call(@str)
72
+ end
73
+ end
74
+
56
75
  TransitionEnd = Transition.new('\A(\/?)\z', trans: 'transition_end')
76
+ TransitionExecuteFunc = InplaceTransition.new(trans: 'transition_execute_func')
57
77
  TransitionMetadata = Transition.new('\A(\/\$metadata)(.*)',
58
78
  trans: 'transition_metadata')
59
79
  TransitionBatch = Transition.new('\A(\/\$batch)(.*)',
data/lib/odata/walker.rb CHANGED
@@ -29,18 +29,24 @@ module Safrano
29
29
 
30
30
  # are $links requested ?
31
31
  attr_reader :do_links
32
+
33
+ attr_reader :request
32
34
 
33
35
  NIL_SERVICE_FATAL = 'Walker is called with a nil service'
34
36
  EMPTYSTR = ''
35
37
  SLASH = '/'
36
38
 
37
- def initialize(service, path, content_id_refs = nil)
39
+ def initialize(service, path, request, content_id_refs = nil )
38
40
  raise NIL_SERVICE_FATAL unless service
39
41
 
40
42
  path = URI.decode_www_form_component(path)
41
43
  @context = service
42
44
  @content_id_refs = content_id_refs
43
-
45
+
46
+ # needed because for function import we need access to the url parameters (req.params)
47
+ # who contains the functions params
48
+ @request = request
49
+
44
50
  @contexts = [@context]
45
51
 
46
52
  @path_start = @path_remain = if service
@@ -109,6 +115,16 @@ module Safrano
109
115
  end
110
116
  end
111
117
 
118
+ # execute function import with request parameters
119
+ # input: @context containt the function to exectute,
120
+ # @request.params should normally contain the params
121
+ # result: validate the params for the given function, execute the function and
122
+ # return it's result back into @context,
123
+ # and finaly set status :end (or error if anyting went wrong )
124
+ def do_run_with_execute_func
125
+ @context, @status, @error = @context.do_execute_func(@request)
126
+ end
127
+
112
128
  # little hacks... depending on returned state, set some attributes
113
129
  def state_mappings
114
130
  case @status
@@ -137,6 +153,8 @@ module Safrano
137
153
  # entity reference here and place it in @context
138
154
  when :run_with_content_id
139
155
  do_run_with_content_id
156
+ when :run_with_execute_func
157
+ do_run_with_execute_func
140
158
  end
141
159
 
142
160
  @contexts << @context
@@ -62,6 +62,8 @@ module Safrano
62
62
  # All tap_valid* handlers are executed
63
63
  # tap_error* handlers are not executed
64
64
  class Valid
65
+ attr_reader :result
66
+
65
67
  def initialize(result)
66
68
  @result = result
67
69
  end
@@ -100,10 +102,6 @@ module Safrano
100
102
  def error
101
103
  nil
102
104
  end
103
-
104
- def result
105
- @result
106
- end
107
105
  end # class Valid
108
106
 
109
107
  def self.valid(result)
@@ -4,7 +4,7 @@ CRLF = "\r\n"
4
4
  LF = "\n"
5
5
 
6
6
  require 'securerandom'
7
- require 'webrick/httpstatus'
7
+ require 'rack/utils'
8
8
 
9
9
  # Simple multipart support for OData $batch purpose
10
10
  module MIME
@@ -457,7 +457,7 @@ module MIME
457
457
  "Content-Transfer-Encoding: binary#{CRLF}",
458
458
  'HTTP/1.1 '].join(CRLF).freeze
459
459
 
460
- StatusMessage = ::WEBrick::HTTPStatus::StatusMessage.freeze
460
+ StatusMessage = ::Rack::Utils::HTTP_STATUS_CODES.freeze
461
461
 
462
462
  def initialize
463
463
  @hd = {}
@@ -99,6 +99,7 @@ module Safrano
99
99
  def create_odata_walker
100
100
  @env['safrano.walker'] = @walker = Walker.new(@service,
101
101
  path_info,
102
+ self,
102
103
  @content_id_references)
103
104
  end
104
105
 
@@ -112,7 +112,7 @@ module Safrano
112
112
  include Safrano
113
113
  include ExpandHandler
114
114
 
115
- XML_PREAMBLE = %Q(<?xml version="1.0" encoding="utf-8" standalone="yes"?>\r\n)
115
+ XML_PREAMBLE = %(<?xml version="1.0" encoding="utf-8" standalone="yes"?>\r\n)
116
116
 
117
117
  # This is just a hash of entity Set Names to the corresponding Class
118
118
  # Example
@@ -137,6 +137,7 @@ module Safrano
137
137
  attr_accessor :relman
138
138
  attr_accessor :complex_types
139
139
  attr_accessor :function_imports
140
+ attr_accessor :function_import_keys
140
141
 
141
142
  # Instance attributes for specialized Version specific Instances
142
143
  attr_accessor :v1
@@ -155,6 +156,7 @@ module Safrano
155
156
  @relman = Safrano::RelationManager.new
156
157
  @complex_types = Set.new
157
158
  @function_imports = {}
159
+ @function_import_keys = []
158
160
  @cmap = {}
159
161
  instance_eval(&block) if block_given?
160
162
  end
@@ -204,6 +206,12 @@ module Safrano
204
206
  (@v2.xserver_url = @xserver_url) if @v2
205
207
  end
206
208
 
209
+ # keep the bug active for now, but allow to activate the fix,
210
+ # later we will change the default to be fixed
211
+ def bugfix_create_response(bool = false)
212
+ @bugfix_create_response = bool
213
+ end
214
+
207
215
  # end public API
208
216
 
209
217
  def set_uribase
@@ -233,6 +241,7 @@ module Safrano
233
241
  other.batch_handler = @batch_handler
234
242
  other.complex_types = @complex_types
235
243
  other.function_imports = @function_imports
244
+ other.function_import_keys = @function_import_keys
236
245
  other
237
246
  end
238
247
 
@@ -316,6 +325,8 @@ module Safrano
316
325
  def function_import(name)
317
326
  funcimp = Safrano::FunctionImport(name)
318
327
  @function_imports[name] = funcimp
328
+ @function_import_keys << name
329
+ set_funcimports_sorted
319
330
  funcimp
320
331
  end
321
332
 
@@ -332,7 +343,11 @@ module Safrano
332
343
  @collections.sort_by! { |klass| klass.entity_set_name.size }.reverse! if @collections
333
344
  @collections
334
345
  end
335
-
346
+
347
+ # need to be sorted by size too
348
+ def set_funcimports_sorted
349
+ @function_import_keys.sort_by!{|k| k.size}.reverse!
350
+ end
336
351
  # to be called at end of publishing block to ensure we get the right names
337
352
  # and additionally build the list of valid attribute path's used
338
353
  # for validation of $orderby or $filter params
@@ -360,7 +375,11 @@ module Safrano
360
375
  # finalize the uri's and include NoMappingBeforeOutput or MappingBeforeOutput as needed
361
376
  @collections.each do |klass|
362
377
  klass.build_uri(@uribase)
363
- klass.include(klass.time_cols.empty? ? Safrano::NoMappingBeforeOutput : Safrano::MappingBeforeOutput)
378
+ # TODO perf
379
+ klass.include( (klass.time_cols.empty? && klass.decimal_cols.empty?) ? Safrano::NoMappingBeforeOutput : Safrano::MappingBeforeOutput)
380
+
381
+ # Output create (POST) as single entity (Standard) or as array (non-standard buggy)
382
+ klass.include ( @bugfix_create_response ? Safrano::EntityCreateStandardOutput : Safrano::EntityCreateArrayOutput)
364
383
  end
365
384
 
366
385
  # build allowed transitions (requires that @collections are filled and sorted for having a
@@ -392,7 +411,7 @@ module Safrano
392
411
  end
393
412
 
394
413
  def base_url_func_regexp
395
- @function_imports.keys.join('|')
414
+ @function_import_keys.join('|')
396
415
  end
397
416
 
398
417
  def service
@@ -486,7 +505,9 @@ module Safrano
486
505
  doc.add_element('edmx:Edmx', 'Version' => '1.0')
487
506
  doc.root.add_namespace('xmlns:edmx', XMLNS::MSFT_ADO_2007_EDMX)
488
507
  serv = doc.root.add_element('edmx:DataServices',
489
- 'm:DataServiceVersion' => '1.0')
508
+ # TODO: export the real version (result from version negotions)
509
+ # but currently we support only v1 and v2, and most users will use v2
510
+ 'm:DataServiceVersion' => '2.0')
490
511
  # 'm:DataServiceVersion' => "#{self.dataServiceVersion}" )
491
512
  # DataServiceVersion: This attribute MUST be in the data service
492
513
  # metadata namespace
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Safrano
4
- VERSION = '0.4.5'
4
+ VERSION = '0.5.3'
5
5
  end
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: safrano
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.4.5
4
+ version: 0.5.3
5
5
  platform: ruby
6
6
  authors:
7
7
  - oz
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2020-12-27 00:00:00.000000000 Z
11
+ date: 2021-07-10 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: rack
@@ -108,7 +108,7 @@ dependencies:
108
108
  - - "~>"
109
109
  - !ruby/object:Gem::Version
110
110
  version: '0.51'
111
- description: Safrano is an OData server library based on Ruby, Rack and Sequel.
111
+ description: Safrano is an OData server library based on Ruby Sequel and Rack.
112
112
  email: dev@aithscel.eu
113
113
  executables: []
114
114
  extensions: []
@@ -189,8 +189,8 @@ required_rubygems_version: !ruby/object:Gem::Requirement
189
189
  - !ruby/object:Gem::Version
190
190
  version: '0'
191
191
  requirements: []
192
- rubygems_version: 3.2.0.rc.2
192
+ rubygems_version: 3.2.5
193
193
  signing_key:
194
194
  specification_version: 4
195
- summary: Safrano is a Ruby OData server library based on Sequel and Rack
195
+ summary: Safrano is an OData server library based on Ruby Sequel and Rack
196
196
  test_files: []