safrano 0.4.4 → 0.5.2

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: 88f62ce611a5030382ea19c2e7c22b47f454b85c51261e5bad060d82e4680dff
4
- data.tar.gz: '088e66ed4a107a3bef8dd6db68c8ca626f7d3acef6ed2e58eb53d871e6349613'
3
+ metadata.gz: 00bf6cd3a561928000b8bf53bbc71f139b162f92adc229e625f010dc6f1ce3c7
4
+ data.tar.gz: bf154e1b55e4d32e4640d6c993566a45eae8a2bce44b23dd979a9946d3633dc7
5
5
  SHA512:
6
- metadata.gz: 2d719e317178a4baebc2e40cc50b95559921fe2936a6a03437f7f92714d173ef6160b10620ce5476ea54a7433b3dd1366b49ec37a48b121b8f808ad030e2c364
7
- data.tar.gz: 535ee0f1d4fed30a0c94918c31149a2e95fce90051548a2ccca6bffb71930c0767543255695496c3623249070b5efb1b6a203a1dae6f868c1e4da9a126a85346
6
+ metadata.gz: 9b2fad0321b45bae50f3435b688b31f99d320cfa4f81f14867f64659faa0c4e8ece7bc4f1f7ecc2db2c7710c08f7726bb4d758bf5d09d487a54199afea50119a
7
+ data.tar.gz: b9a70b4a08fb7a2cc31df7e7637f635a42a85433660f5061f6897e1fb463496252c8d611fb1023e5f27202bbc478b58569d2ad42b1c3359db14e34c92f4ec35c
@@ -53,7 +53,7 @@ module Safrano
53
53
  # methods related to transitions to next state (cf. walker)
54
54
  module Transitions
55
55
  def transition_end(_match_result)
56
- [nil, :end]
56
+ Transition::RESULT_END
57
57
  end
58
58
 
59
59
  def transition_value(_match_result)
@@ -64,7 +64,7 @@ module Safrano
64
64
  Safrano::TransitionValue].freeze
65
65
 
66
66
  def allowed_transitions
67
- ALLOWED_TRANSITIONS
67
+ Transitions::ALLOWED_TRANSITIONS
68
68
  end
69
69
  end
70
70
  include Transitions
@@ -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
@@ -77,17 +80,20 @@ module Safrano
77
80
  end
78
81
  end
79
82
 
80
- D = 'd'.freeze
81
- DJ_OPEN = '{"d":'.freeze
82
- DJ_CLOSE = '}'.freeze
83
+ D = 'd'
84
+ DJ_OPEN = '{"d":'
85
+ DJ_CLOSE = '}'
83
86
  EMPTYH = {}.freeze
84
87
 
85
88
  def to_odata_json(request:)
86
89
  template = @modelk.output_template(expand_list: @uparms.expand.template,
87
90
  select: @uparms.select)
91
+ # TODO: Error handling if database contains binary BLOB data that cant be
92
+ # interpreted as UTF-8 then JSON will fail here
88
93
  innerj = request.service.get_coll_odata_h(array: @cx.all,
89
94
  template: template,
90
95
  icount: @inlinecount).to_json
96
+
91
97
  "#{DJ_OPEN}#{innerj}#{DJ_CLOSE}"
92
98
  end
93
99
 
@@ -128,9 +134,9 @@ module Safrano
128
134
 
129
135
  # on model class level we return the collection
130
136
  def odata_get(req)
131
- @params = req.params
132
- initialize_dataset
133
-
137
+ @params = @params || req.params
138
+ initialize_dataset
139
+ initialize_uparms
134
140
  @uparms.check_all.if_valid { |_ret|
135
141
  odata_get_apply_params.if_valid { |_ret|
136
142
  odata_get_output(req)
@@ -167,8 +173,7 @@ module Safrano
167
173
  end
168
174
 
169
175
  def initialize_dataset(dtset = nil)
170
- @cx = dtset || navigated_dataset
171
- @uparms = UrlParameters4Coll.new(@cx, @params)
176
+ @cx = @cx || dtset || navigated_dataset
172
177
  end
173
178
  # redefinitions of the main methods for a navigated collection
174
179
  # (eg. all Books of Author[2] is Author[2].Books.all )
@@ -1,9 +1,9 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require 'odata/error.rb'
3
+ require 'odata/error'
4
4
 
5
- require_relative 'filter/parse.rb'
6
- require_relative 'filter/sequel.rb'
5
+ require_relative 'filter/parse'
6
+ require_relative 'filter/sequel'
7
7
 
8
8
  # filter base class and subclass in our OData namespace
9
9
  module Safrano
@@ -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 do |ast| ast.sequel_expr(jh) end
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! do |f| dtcx.where(f) end
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
@@ -2,12 +2,17 @@
2
2
 
3
3
  require 'rack'
4
4
  require 'fileutils'
5
- require_relative './navigation_attribute.rb'
5
+ require_relative './navigation_attribute'
6
6
 
7
7
  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
@@ -24,9 +29,16 @@ module Safrano
24
29
 
25
30
  def register
26
31
  @abs_klass_dir = File.absolute_path(@media_dir_name, @root)
32
+ end
33
+
34
+ def create_abs_class_dir
27
35
  FileUtils.makedirs @abs_klass_dir unless Dir.exist?(@abs_klass_dir)
28
36
  end
29
37
 
38
+ def finalize
39
+ create_abs_class_dir
40
+ end
41
+
30
42
  # minimal working implementation...
31
43
  # Note: @file_server works relative to @root directory
32
44
  def odata_get(request:, entity:)
@@ -47,13 +59,13 @@ module Safrano
47
59
  end
48
60
 
49
61
  # relative to @root
50
- # eg Photo/1/pommes-topaz.jpg
62
+ # eg Photo/1/1
51
63
  def filename(entity)
52
64
  Dir.chdir(abs_path(entity)) do
53
65
  # simple design: one file per directory, and the directory
54
66
  # contains the media entity-id --> implicit link between the media
55
67
  # entity
56
- File.join(media_path(entity), Dir.glob('*').sort.first)
68
+ File.join(media_path(entity), Dir.glob('*').max)
57
69
  end
58
70
  end
59
71
 
@@ -62,6 +74,11 @@ module Safrano
62
74
  File.absolute_path(media_path(entity), @root)
63
75
  end
64
76
 
77
+ # absolute filename
78
+ def abs_filename(entity)
79
+ File.absolute_path(filename(entity), @root)
80
+ end
81
+
65
82
  # this is relative to abs_klass_dir(entity) eg to /@root/Photo
66
83
  # simplest implementation is media_directory = entity.media_path_id
67
84
  # --> we get a 1 level depth flat directory structure
@@ -101,7 +118,7 @@ module Safrano
101
118
  def ressource_version(entity)
102
119
  Dir.chdir(@abs_klass_dir) do
103
120
  in_media_directory(entity) do
104
- Dir.glob('*').sort.last
121
+ Dir.glob('*').max
105
122
  end
106
123
  end
107
124
  end
@@ -135,15 +152,14 @@ module Safrano
135
152
 
136
153
  # this is relative to abs_klass_dir(entity) eg to /@root/Photo
137
154
  # tree-structure
138
- # media_path_ids = 1 --> 1
139
- # media_path_ids = 15 --> 1/5
140
- # media_path_ids = 555 --> 5/5/5
141
- # media_path_ids = 5,5,5 --> 5/00/5/00/5
142
- # media_path_ids = 5,00,5 --> 5/00/0/0/00/5
143
- # 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
144
161
  def media_directory(entity)
145
162
  StaticTree.path_builder(entity.media_path_ids)
146
- # entity.media_path_ids.map{|id| id.to_s.chars.join('/')}.join(@sep)
147
163
  end
148
164
 
149
165
  def in_media_directory(entity)
@@ -201,9 +217,14 @@ module Safrano
201
217
  @media_handler = Safrano::Media::Static.new(mediaklass: self)
202
218
  end
203
219
 
220
+ def finalize_media
221
+ @media_handler.finalize
222
+ end
223
+
204
224
  def use(klass, args)
205
225
  args[:mediaklass] = self
206
226
  @media_handler = klass.new(**args)
227
+ @media_handler.create_abs_class_dir
207
228
  end
208
229
 
209
230
  # API method for setting the model field mapped to SLUG on upload
@@ -250,22 +271,39 @@ module Safrano
250
271
  missing: :skip)
251
272
  end
252
273
 
253
- # to_one rels are create with FK data set on the parent entity
254
- if parent
255
- odata_create_save_entity_and_rel(req, new_entity, assoc, parent)
256
- else
257
- # in-changeset requests get their own transaction
258
- new_entity.save(transaction: !req.in_changeset)
259
- 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)
290
+
291
+ # call before_create_media hook
292
+ new_entity.before_create_media if new_entity.respond_to? :before_create_media
293
+
294
+ media_handler.save_file(data: data,
295
+ entity: new_entity,
296
+ filename: filename)
297
+
298
+ # call after_create_media hook
299
+ new_entity.after_create_media if new_entity.respond_to? :after_create_media
260
300
 
261
- req.register_content_id_ref(new_entity)
262
- new_entity.copy_request_infos(req)
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
263
306
 
264
- media_handler.save_file(data: data,
265
- entity: new_entity,
266
- filename: filename)
267
- # json is default content type so we dont need to specify it here again
268
- [201, EMPTY_HASH, new_entity.to_odata_post_json(service: req.service)]
269
307
  else # TODO: other formats
270
308
  415
271
309
  end
@@ -2,76 +2,184 @@
2
2
 
3
3
  module Safrano
4
4
  module FunctionImport
5
+ EMPTY_HASH = {}.freeze
5
6
  class ResultDefinition
6
- D = 'd'.freeze
7
- DJ_OPEN = '{"d":'.freeze
8
- DJ_CLOSE = '}'.freeze
9
- METAK = '__metadata'.freeze
10
- TYPEK = 'type'.freeze
11
- VALUEK = 'value'.freeze
12
- RESULTSK = 'results'.freeze
13
- COLLECTION = 'Collection'.freeze
14
-
15
- def initialize(klassmod)
16
- @klassmod = klassmod
7
+ D = 'd'
8
+ DJ_OPEN = '{"d":'
9
+ DJ_CLOSE = '}'
10
+ METAK = '__metadata'
11
+ TYPEK = 'type'
12
+ VALUEK = 'value'
13
+ RESULTSK = 'results'
14
+ COLLECTION = 'Collection'
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,19 +198,52 @@ 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
98
239
  @values = {}
99
240
  end
100
- METAK = '__metadata'.freeze
101
- TYPEK = 'type'.freeze
241
+ METAK = '__metadata'
242
+ TYPEK = 'type'
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