safrano 0.4.5 → 0.5.3

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 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: []