safrano 0.4.1 → 0.4.6

Sign up to get free protection for your applications and to get access to all the features.
Files changed (54) hide show
  1. checksums.yaml +4 -4
  2. data/lib/core_ext/Dir/iter.rb +18 -0
  3. data/lib/core_ext/Hash/transform.rb +21 -0
  4. data/lib/core_ext/Integer/edm.rb +13 -0
  5. data/lib/core_ext/REXML/Document/output.rb +16 -0
  6. data/lib/core_ext/String/convert.rb +25 -0
  7. data/lib/core_ext/String/edm.rb +13 -0
  8. data/lib/core_ext/dir.rb +3 -0
  9. data/lib/core_ext/hash.rb +3 -0
  10. data/lib/core_ext/integer.rb +3 -0
  11. data/lib/core_ext/rexml.rb +3 -0
  12. data/lib/core_ext/string.rb +5 -0
  13. data/lib/odata/attribute.rb +15 -10
  14. data/lib/odata/batch.rb +15 -13
  15. data/lib/odata/collection.rb +144 -535
  16. data/lib/odata/collection_filter.rb +47 -40
  17. data/lib/odata/collection_media.rb +155 -99
  18. data/lib/odata/collection_order.rb +50 -37
  19. data/lib/odata/common_logger.rb +36 -34
  20. data/lib/odata/complex_type.rb +152 -0
  21. data/lib/odata/edm/primitive_types.rb +184 -0
  22. data/lib/odata/entity.rb +183 -216
  23. data/lib/odata/error.rb +195 -31
  24. data/lib/odata/expand.rb +126 -0
  25. data/lib/odata/filter/base.rb +74 -0
  26. data/lib/odata/filter/error.rb +49 -6
  27. data/lib/odata/filter/parse.rb +44 -36
  28. data/lib/odata/filter/sequel.rb +136 -67
  29. data/lib/odata/filter/sequel_function_adapter.rb +148 -0
  30. data/lib/odata/filter/token.rb +26 -19
  31. data/lib/odata/filter/tree.rb +113 -63
  32. data/lib/odata/function_import.rb +168 -0
  33. data/lib/odata/model_ext.rb +639 -0
  34. data/lib/odata/navigation_attribute.rb +44 -61
  35. data/lib/odata/relations.rb +5 -5
  36. data/lib/odata/select.rb +54 -0
  37. data/lib/odata/transition.rb +71 -0
  38. data/lib/odata/url_parameters.rb +128 -37
  39. data/lib/odata/walker.rb +20 -10
  40. data/lib/safrano.rb +17 -37
  41. data/lib/safrano/contract.rb +143 -0
  42. data/lib/safrano/core.rb +29 -104
  43. data/lib/safrano/core_ext.rb +13 -0
  44. data/lib/safrano/deprecation.rb +73 -0
  45. data/lib/safrano/multipart.rb +39 -43
  46. data/lib/safrano/rack_app.rb +68 -67
  47. data/lib/safrano/{odata_rack_builder.rb → rack_builder.rb} +18 -2
  48. data/lib/safrano/request.rb +102 -51
  49. data/lib/safrano/response.rb +5 -3
  50. data/lib/safrano/sequel_join_by_paths.rb +2 -2
  51. data/lib/safrano/service.rb +274 -219
  52. data/lib/safrano/version.rb +3 -1
  53. data/lib/sequel/plugins/join_by_paths.rb +17 -29
  54. metadata +34 -11
@@ -1,61 +1,68 @@
1
- require 'odata/error.rb'
1
+ # frozen_string_literal: true
2
2
 
3
- require_relative 'filter/parse.rb'
4
- require_relative 'filter/sequel.rb'
3
+ require 'odata/error'
5
4
 
6
- # a few helper method
7
- class String
8
- MASK_RGX = /'((?:[^']|(?:\'{2}))*)'/.freeze
9
- UNMASK_RGX = /'(%?)(\$\d+)(%?)'/.freeze
10
- def with_mask_quoted_substrings!
11
- cnt = 0
12
- repl = {}
13
- gsub!(MASK_RGX) do |_m|
14
- cnt += 1
15
- repl["$#{cnt}"] = Regexp.last_match(1)
16
- "'$#{cnt}'"
5
+ require_relative 'filter/parse'
6
+ require_relative 'filter/sequel'
7
+
8
+ # filter base class and subclass in our OData namespace
9
+ module Safrano
10
+ class FilterBase
11
+ # re-useable empty filtering (idempotent)
12
+ EmptyFilter = new.freeze
13
+
14
+ def self.factory(filterstr)
15
+ filterstr.nil? ? EmptyFilter : FilterByParse.new(filterstr)
16
+ end
17
+
18
+ def apply_to_dataset(dtcx)
19
+ Contract.valid(dtcx)
17
20
  end
18
- yield self
19
21
 
20
- gsub!(UNMASK_RGX) do |_m|
21
- k = Regexp.last_match(2).to_s
22
- "'#{Regexp.last_match(1)}#{repl[k]}#{Regexp.last_match(3)}'"
22
+ # finalize
23
+ def finalize(_jh) Contract::OK end
24
+
25
+ def empty?
26
+ true
23
27
  end
24
- end
25
28
 
26
- def with_mask_quoted_substrings
27
- cnt = 0
28
- repl = {}
29
- tmpstr = gsub(MASK_RGX) do |_m|
30
- cnt += 1
31
- repl["$#{cnt}"] = Regexp.last_match(1)
32
- "'$#{cnt}'"
29
+ def parse_error?
30
+ false
33
31
  end
34
- yield tmpstr
35
32
  end
36
- end
37
33
 
38
- # filter base class and subclass in our OData namespace
39
- module OData
40
34
  # should handle everything by parsing
41
- class FilterByParse
42
- def initialize(filterstr, jh)
35
+ class FilterByParse < FilterBase
36
+ attr_reader :filterstr
37
+
38
+ def initialize(filterstr)
43
39
  @filterstr = filterstr.dup
44
- @ast = OData::Filter::Parser.new(@filterstr).build
45
- @jh = jh
40
+ @ast = Safrano::Filter::Parser.new(@filterstr).build
46
41
  end
47
42
 
48
- def apply_to_dataset(dtcx)
49
- filtexpr = @ast.sequel_expr(@jh)
50
- dtcx = @jh.dataset(dtcx).where(filtexpr).select_all(@jh.start_model.table_name)
43
+ # this build's up the Sequel Filter Expression, and as a side effect,
44
+ # it also finalizes the join helper that we need for the start dataset join
45
+ # the join-helper is shared by the order-by object and was potentially already
46
+ # partly built on order-by object creation.
47
+ def finalize(jh)
48
+ @filtexpr = @ast.if_valid { |ast| ast.sequel_expr(jh) }
51
49
  end
52
50
 
53
- def sequel_expr
54
- @ast.sequel_expr(@jh)
51
+ def apply_to_dataset(dtcx)
52
+ # normally finalize is called before, and thus @filtexpr is set
53
+ @filtexpr.map_result! { |f| dtcx.where(f) }
55
54
  end
56
55
 
56
+ # Note: this is really only *parse* error, ie the error encounterd while
57
+ # trying to build the AST
58
+ # Later when evaluating the AST, there can be other errors, they shall
59
+ # be tracked with @error
57
60
  def parse_error?
58
- @ast.kind_of? StandardError
61
+ @ast.error
62
+ end
63
+
64
+ def empty?
65
+ false
59
66
  end
60
67
  end
61
68
  end
@@ -1,29 +1,44 @@
1
+ # frozen_string_literal: true
2
+
1
3
  require 'rack'
2
4
  require 'fileutils'
3
- require_relative './navigation_attribute.rb'
5
+ require_relative './navigation_attribute'
4
6
 
5
- module OData
7
+ module Safrano
6
8
  module Media
7
9
  # base class for Media Handler
8
10
  class Handler
11
+ def check_before_create(data:,
12
+ entity:,
13
+ filename:)
14
+ Contract::OK
15
+ end
9
16
  end
10
17
 
11
18
  # Simple static File/Directory based media store handler
12
19
  # similar to Rack::Static
13
20
  # with a flat directory structure
14
21
  class Static < Handler
15
-
16
- def initialize(root: nil)
22
+ def initialize(root: nil, mediaklass:)
17
23
  @root = File.absolute_path(root || Dir.pwd)
18
24
  @file_server = ::Rack::File.new(@root)
25
+ @media_class = mediaklass
26
+ @media_dir_name = mediaklass.to_s
27
+ register
28
+ end
29
+
30
+ def register
31
+ @abs_klass_dir = File.absolute_path(@media_dir_name, @root)
32
+ end
33
+
34
+ def create_abs_class_dir
35
+ FileUtils.makedirs @abs_klass_dir unless Dir.exist?(@abs_klass_dir)
19
36
  end
20
37
 
21
- # TODO testcase and better abs_klass_dir design
22
- def register(klass)
23
- abs_klass_dir = File.absolute_path(klass.type_name, @root)
24
- FileUtils.makedirs abs_klass_dir unless Dir.exists?(abs_klass_dir)
38
+ def finalize
39
+ create_abs_class_dir
25
40
  end
26
-
41
+
27
42
  # minimal working implementation...
28
43
  # Note: @file_server works relative to @root directory
29
44
  def odata_get(request:, entity:)
@@ -37,35 +52,33 @@ module OData
37
52
  fsret
38
53
  end
39
54
 
40
- # TODO perf: this can be precalculated and cached on MediaModelKlass level
41
- # and passed as argument to save_file
42
- # eg. /@root/Photo
43
- def abs_klass_dir(entity)
44
- File.absolute_path(entity.klass_dir, @root)
45
- end
46
-
47
55
  # this is relative to @root
48
56
  # eg. Photo/1
49
57
  def media_path(entity)
50
- File.join(entity.klass_dir, media_directory(entity))
58
+ File.join(@media_dir_name, media_directory(entity))
51
59
  end
52
-
60
+
53
61
  # relative to @root
54
- # eg Photo/1/pommes-topaz.jpg
62
+ # eg Photo/1/1
55
63
  def filename(entity)
56
64
  Dir.chdir(abs_path(entity)) do
57
65
  # simple design: one file per directory, and the directory
58
66
  # contains the media entity-id --> implicit link between the media
59
67
  # entity
60
- File.join(media_path(entity), Dir.glob('*').first)
68
+ File.join(media_path(entity), Dir.glob('*').max)
61
69
  end
62
70
  end
63
-
71
+
64
72
  # /@root/Photo/1
65
73
  def abs_path(entity)
66
74
  File.absolute_path(media_path(entity), @root)
67
75
  end
68
76
 
77
+ # absolute filename
78
+ def abs_filename(entity)
79
+ File.absolute_path(filename(entity), @root)
80
+ end
81
+
69
82
  # this is relative to abs_klass_dir(entity) eg to /@root/Photo
70
83
  # simplest implementation is media_directory = entity.media_path_id
71
84
  # --> we get a 1 level depth flat directory structure
@@ -75,23 +88,23 @@ module OData
75
88
 
76
89
  def in_media_directory(entity)
77
90
  mpi = media_directory(entity)
78
- Dir.mkdir mpi unless Dir.exists?(mpi)
91
+ Dir.mkdir mpi unless Dir.exist?(mpi)
79
92
  Dir.chdir mpi do
80
93
  yield
81
94
  end
82
95
  end
83
96
 
84
- def odata_delete(request:, entity:)
85
- Dir.chdir(abs_klass_dir(entity)) do
97
+ def odata_delete(entity:)
98
+ Dir.chdir(@abs_klass_dir) do
86
99
  in_media_directory(entity) do
87
100
  Dir.glob('*').each { |oldf| File.delete(oldf) }
88
101
  end
89
- end
102
+ end
90
103
  end
91
104
 
92
105
  # Here as well, MVP implementation
93
106
  def save_file(data:, filename:, entity:)
94
- Dir.chdir(abs_klass_dir(entity)) do
107
+ Dir.chdir(@abs_klass_dir) do
95
108
  in_media_directory(entity) do
96
109
  filename = '1'
97
110
  File.open(filename, 'wb') { |f| IO.copy_stream(data, f) }
@@ -103,19 +116,22 @@ module OData
103
116
  # after each upload, so that clients get informed about new versions
104
117
  # of the same media ressource
105
118
  def ressource_version(entity)
106
- Dir.chdir(abs_klass_dir(entity)) do
119
+ Dir.chdir(@abs_klass_dir) do
107
120
  in_media_directory(entity) do
108
- Dir.glob('*').last
121
+ Dir.glob('*').max
109
122
  end
110
- end
123
+ end
111
124
  end
112
-
125
+
113
126
  # Here as well, MVP implementation
114
127
  def replace_file(data:, filename:, entity:)
115
- Dir.chdir(abs_klass_dir(entity)) do
128
+ Dir.chdir(@abs_klass_dir) do
116
129
  in_media_directory(entity) do
117
130
  version = nil
118
- Dir.glob('*').each { |oldf| version = oldf; File.delete(oldf) }
131
+ Dir.glob('*').sort.each do |oldf|
132
+ version = oldf
133
+ File.delete(oldf)
134
+ end
119
135
  filename = (version.to_i + 1).to_s
120
136
  File.open(filename, 'wb') { |f| IO.copy_stream(data, f) }
121
137
  end
@@ -125,107 +141,105 @@ module OData
125
141
  # Simple static File/Directory based media store handler
126
142
  # similar to Rack::Static
127
143
  # with directory Tree structure
128
-
144
+
129
145
  class StaticTree < Static
130
-
131
- SEP = '/00/'
132
-
133
- def StaticTree.path_builder(ids)
134
- ids.map{|id| id.to_s.chars.join('/')}.join(SEP) + '/v'
146
+ SEP = '/00/'.freeze
147
+ VERS = '/v'.freeze
148
+
149
+ def self.path_builder(ids)
150
+ ids.map { |id| id.to_s.chars.join('/') }.join(SEP) << VERS
135
151
  end
136
-
152
+
137
153
  # this is relative to abs_klass_dir(entity) eg to /@root/Photo
138
154
  # tree-structure
139
- # media_path_ids = 1 --> 1
140
- # media_path_ids = 15 --> 1/5
141
- # media_path_ids = 555 --> 5/5/5
142
- # media_path_ids = 5,5,5 --> 5/00/5/00/5
143
- # media_path_ids = 5,00,5 --> 5/00/0/0/00/5
144
- # 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
145
161
  def media_directory(entity)
146
162
  StaticTree.path_builder(entity.media_path_ids)
147
- # entity.media_path_ids.map{|id| id.to_s.chars.join('/')}.join(@sep)
148
163
  end
149
164
 
150
165
  def in_media_directory(entity)
151
166
  mpi = media_directory(entity)
152
- FileUtils.makedirs mpi unless Dir.exists?(mpi)
153
- Dir.chdir mpi do
154
- yield
155
- end
167
+ FileUtils.makedirs mpi unless Dir.exist?(mpi)
168
+ Dir.chdir(mpi) { yield }
156
169
  end
157
170
 
158
- def odata_delete(request:, entity:)
159
- Dir.chdir(abs_klass_dir(entity)) do
171
+ def odata_delete(entity:)
172
+ Dir.chdir(@abs_klass_dir) do
160
173
  in_media_directory(entity) do
161
- Dir.glob('*').each { |oldf| File.delete(oldf) if File.file?(oldf) }
174
+ Dir.glob('*').sort.each { |oldf| File.delete(oldf) if File.file?(oldf) }
162
175
  end
163
- end
176
+ end
164
177
  end
165
-
178
+
179
+ # Here as well, MVP implementation
180
+ # def replace_file(data:, filename:, entity:)
181
+ # Dir.chdir(abs_klass_dir(entity)) do
182
+ # in_media_directory(entity) do
183
+ # Dir.glob('*').each { |oldf| File.delete(oldf) if File.file?(oldf) }
184
+ # File.open(filename, 'wb') { |f| IO.copy_stream(data, f) }
185
+ # end
186
+ # end
187
+ # end
166
188
  # Here as well, MVP implementation
167
- # def replace_file(data:, filename:, entity:)
168
- # Dir.chdir(abs_klass_dir(entity)) do
169
- # in_media_directory(entity) do
170
- # Dir.glob('*').each { |oldf| File.delete(oldf) if File.file?(oldf) }
171
- # File.open(filename, 'wb') { |f| IO.copy_stream(data, f) }
172
- # end
173
- # end
174
- # end
175
- # Here as well, MVP implementation
176
189
  def replace_file(data:, filename:, entity:)
177
- Dir.chdir(abs_klass_dir(entity)) do
190
+ Dir.chdir(@abs_klass_dir) do
178
191
  in_media_directory(entity) do
179
192
  version = nil
180
- Dir.glob('*').each { |oldf| version = oldf; File.delete(oldf) }
193
+ Dir.glob('*').sort.each do |oldf|
194
+ version = oldf
195
+ File.delete(oldf)
196
+ end
181
197
  filename = (version.to_i + 1).to_s
182
198
  File.open(filename, 'wb') { |f| IO.copy_stream(data, f) }
183
199
  end
184
200
  end
185
201
  end
186
-
187
202
  end
188
-
189
203
  end
190
204
 
191
205
  # special handling for media entity
192
206
  module EntityClassMedia
193
207
  attr_reader :media_handler
194
208
  attr_reader :slug_field
195
-
209
+
196
210
  # API method for defining the media handler
197
211
  # eg.
198
212
  # publish_media_model photos do
199
- # use OData::Media::Static, :root => '/media_root'
213
+ # use Safrano::Media::Static, :root => '/media_root'
200
214
  # end
201
215
 
202
216
  def set_default_media_handler
203
- @media_handler = OData::Media::Static.new
217
+ @media_handler = Safrano::Media::Static.new(mediaklass: self)
218
+ end
219
+
220
+ def finalize_media
221
+ @media_handler.finalize
204
222
  end
205
223
 
206
224
  def use(klass, args)
225
+ args[:mediaklass] = self
207
226
  @media_handler = klass.new(**args)
227
+ @media_handler.create_abs_class_dir
208
228
  end
209
229
 
210
230
  # API method for setting the model field mapped to SLUG on upload
211
231
  def slug(inp)
212
- @slug_field = inp
232
+ @slug_field = inp
213
233
  end
214
-
234
+
215
235
  def api_check_media_fields
216
- unless self.db_schema.has_key?(:content_type)
217
- raise OData::API::MediaModelError, self
218
- end
219
- # unless self.db_schema.has_key?(:media_src)
220
- # raise OData::API::MediaModelError, self
221
- # end
236
+ raise(Safrano::API::MediaModelError, self) unless db_schema.key?(:content_type)
222
237
  end
223
238
 
224
- # END API methods
239
+ # END API methods
225
240
 
226
241
  def new_media_entity(mimetype:)
227
- nh = {}
228
- nh['content_type'] = mimetype
242
+ nh = { 'content_type' => mimetype }
229
243
  new_from_hson_h(nh)
230
244
  end
231
245
 
@@ -238,11 +252,11 @@ module OData
238
252
  # NOTE: we will implement this first in a MVP way. There will be plenty of
239
253
  # potential future enhancements (performance, scallability, flexibility... with added complexity)
240
254
  # 4. create relation to parent if needed
241
- def odata_create_entity_and_relation(req, assoc, parent)
255
+ def odata_create_entity_and_relation(req, assoc = nil, parent = nil)
242
256
  req.with_media_data do |data, mimetype, filename|
243
257
  ## future enhancement: validate allowed mimetypes ?
244
258
  # if (invalid = invalid_media_mimetype(mimetype))
245
- # ::OData::Request::ON_CGST_ERROR.call(req)
259
+ # ::Safrano::Request::ON_CGST_ERROR.call(req)
246
260
  # return [422, {}, ['Invalid mime type: ', invalid.to_s]]
247
261
  # end
248
262
 
@@ -252,25 +266,44 @@ module OData
252
266
 
253
267
  if slug_field
254
268
 
255
- new_entity.set_fields({ slug_field => filename},
256
- data_fields,
257
- missing: :skip)
258
- end
259
-
260
- # to_one rels are create with FK data set on the parent entity
261
- if parent
262
- odata_create_save_entity_and_rel(req, new_entity, assoc, parent)
263
- else
264
- # in-changeset requests get their own transaction
265
- new_entity.save(transaction: !req.in_changeset)
269
+ new_entity.set_fields({ slug_field => filename },
270
+ data_fields,
271
+ missing: :skip)
266
272
  end
267
273
 
268
- req.register_content_id_ref(new_entity)
269
- new_entity.copy_request_infos(req)
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)
270
297
 
271
- media_handler.save_file(data: data, entity: new_entity, filename: filename)
298
+ # call after_create_media hook
299
+ new_entity.after_create_media if new_entity.respond_to? :after_create_media
300
+
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
272
306
 
273
- [201, CT_JSON, new_entity.to_odata_post_json(service: req.service)]
274
307
  else # TODO: other formats
275
308
  415
276
309
  end
@@ -278,3 +311,26 @@ module OData
278
311
  end
279
312
  end
280
313
  end
314
+
315
+ # deprecated
316
+ # REMOVE 0.6
317
+ module OData
318
+ module Media
319
+ class Static < ::Safrano::Media::Static
320
+ def initialize(root: nil, mediaklass:)
321
+ ::Safrano::Deprecation.deprecate('OData::Media::Static',
322
+ 'Use Safrano::Media::Static instead')
323
+
324
+ super
325
+ end
326
+ end
327
+ class StaticTree < ::Safrano::Media::StaticTree
328
+ def initialize(root: nil, mediaklass:)
329
+ ::Safrano::Deprecation.deprecate('OData::Media::StaticTree',
330
+ 'Use Safrano::Media::StaticTree instead')
331
+
332
+ super
333
+ end
334
+ end
335
+ end
336
+ end