safrano 0.4.0 → 0.4.5

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 +145 -74
  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 +151 -197
  23. data/lib/odata/error.rb +175 -32
  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 +637 -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 +19 -11
  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 +264 -220
  52. data/lib/safrano/version.rb +3 -1
  53. data/lib/sequel/plugins/join_by_paths.rb +17 -29
  54. metadata +34 -12
@@ -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,8 +1,10 @@
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
@@ -12,39 +14,45 @@ module OData
12
14
  # similar to Rack::Static
13
15
  # with a flat directory structure
14
16
  class Static < Handler
15
-
16
- def initialize(root: nil)
17
+ def initialize(root: nil, mediaklass:)
17
18
  @root = File.absolute_path(root || Dir.pwd)
18
19
  @file_server = ::Rack::File.new(@root)
20
+ @media_class = mediaklass
21
+ @media_dir_name = mediaklass.to_s
22
+ register
23
+ end
24
+
25
+ def register
26
+ @abs_klass_dir = File.absolute_path(@media_dir_name, @root)
19
27
  end
20
28
 
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)
29
+ def create_abs_class_dir
30
+ FileUtils.makedirs @abs_klass_dir unless Dir.exist?(@abs_klass_dir)
25
31
  end
26
-
32
+
33
+ def finalize
34
+ create_abs_class_dir
35
+ end
36
+
27
37
  # minimal working implementation...
28
38
  # Note: @file_server works relative to @root directory
29
39
  def odata_get(request:, entity:)
30
40
  media_env = request.env.dup
31
41
  media_env['PATH_INFO'] = filename(entity)
32
- @file_server.call(media_env)
33
- end
34
-
35
- # TODO perf: this can be precalculated and cached on MediaModelKlass level
36
- # and passed as argument to save_file
37
- # eg. /@root/Photo
38
- def abs_klass_dir(entity)
39
- File.absolute_path(entity.klass_dir, @root)
42
+ fsret = @file_server.call(media_env)
43
+ if fsret.first == 200
44
+ # provide own content type as we keep it in the media entity
45
+ fsret[1]['Content-Type'] = entity.content_type
46
+ end
47
+ fsret
40
48
  end
41
49
 
42
50
  # this is relative to @root
43
51
  # eg. Photo/1
44
52
  def media_path(entity)
45
- File.join(entity.klass_dir, media_directory(entity))
53
+ File.join(@media_dir_name, media_directory(entity))
46
54
  end
47
-
55
+
48
56
  # relative to @root
49
57
  # eg Photo/1/pommes-topaz.jpg
50
58
  def filename(entity)
@@ -52,15 +60,20 @@ module OData
52
60
  # simple design: one file per directory, and the directory
53
61
  # contains the media entity-id --> implicit link between the media
54
62
  # entity
55
- File.join(media_path(entity), Dir.glob('*').first)
63
+ File.join(media_path(entity), Dir.glob('*').min)
56
64
  end
57
65
  end
58
-
66
+
59
67
  # /@root/Photo/1
60
68
  def abs_path(entity)
61
69
  File.absolute_path(media_path(entity), @root)
62
70
  end
63
71
 
72
+ # absolute filename
73
+ def abs_filename(entity)
74
+ File.absolute_path(filename(entity), @root)
75
+ end
76
+
64
77
  # this is relative to abs_klass_dir(entity) eg to /@root/Photo
65
78
  # simplest implementation is media_directory = entity.media_path_id
66
79
  # --> we get a 1 level depth flat directory structure
@@ -70,34 +83,51 @@ module OData
70
83
 
71
84
  def in_media_directory(entity)
72
85
  mpi = media_directory(entity)
73
- Dir.mkdir mpi unless Dir.exists?(mpi)
86
+ Dir.mkdir mpi unless Dir.exist?(mpi)
74
87
  Dir.chdir mpi do
75
88
  yield
76
89
  end
77
90
  end
78
91
 
79
- def odata_delete(request:, entity:)
80
- Dir.chdir(abs_klass_dir(entity)) do
92
+ def odata_delete(entity:)
93
+ Dir.chdir(@abs_klass_dir) do
81
94
  in_media_directory(entity) do
82
95
  Dir.glob('*').each { |oldf| File.delete(oldf) }
83
96
  end
84
- end
97
+ end
85
98
  end
86
99
 
87
100
  # Here as well, MVP implementation
88
101
  def save_file(data:, filename:, entity:)
89
- Dir.chdir(abs_klass_dir(entity)) do
102
+ Dir.chdir(@abs_klass_dir) do
90
103
  in_media_directory(entity) do
104
+ filename = '1'
91
105
  File.open(filename, 'wb') { |f| IO.copy_stream(data, f) }
92
106
  end
93
107
  end
94
108
  end
95
109
 
110
+ # needed for having a changing media ressource "source" metadata
111
+ # after each upload, so that clients get informed about new versions
112
+ # of the same media ressource
113
+ def ressource_version(entity)
114
+ Dir.chdir(@abs_klass_dir) do
115
+ in_media_directory(entity) do
116
+ Dir.glob('*').max
117
+ end
118
+ end
119
+ end
120
+
96
121
  # Here as well, MVP implementation
97
122
  def replace_file(data:, filename:, entity:)
98
- Dir.chdir(abs_klass_dir(entity)) do
123
+ Dir.chdir(@abs_klass_dir) do
99
124
  in_media_directory(entity) do
100
- Dir.glob('*').each { |oldf| File.delete(oldf) }
125
+ version = nil
126
+ Dir.glob('*').sort.each do |oldf|
127
+ version = oldf
128
+ File.delete(oldf)
129
+ end
130
+ filename = (version.to_i + 1).to_s
101
131
  File.open(filename, 'wb') { |f| IO.copy_stream(data, f) }
102
132
  end
103
133
  end
@@ -106,15 +136,15 @@ module OData
106
136
  # Simple static File/Directory based media store handler
107
137
  # similar to Rack::Static
108
138
  # with directory Tree structure
109
-
139
+
110
140
  class StaticTree < Static
111
-
112
- SEP = '/00/'
113
-
114
- def StaticTree.path_builder(ids)
115
- ids.map{|id| id.to_s.chars.join('/')}.join(SEP)
141
+ SEP = '/00/'.freeze
142
+ VERS = '/v'.freeze
143
+
144
+ def self.path_builder(ids)
145
+ ids.map { |id| id.to_s.chars.join('/') }.join(SEP) << VERS
116
146
  end
117
-
147
+
118
148
  # this is relative to abs_klass_dir(entity) eg to /@root/Photo
119
149
  # tree-structure
120
150
  # media_path_ids = 1 --> 1
@@ -125,78 +155,87 @@ module OData
125
155
  # media_path_ids = 5,xyz,5 --> 5/00/x/y/z/00/5
126
156
  def media_directory(entity)
127
157
  StaticTree.path_builder(entity.media_path_ids)
128
- # entity.media_path_ids.map{|id| id.to_s.chars.join('/')}.join(@sep)
158
+ # entity.media_path_ids.map{|id| id.to_s.chars.join('/')}.join(@sep)
129
159
  end
130
160
 
131
161
  def in_media_directory(entity)
132
162
  mpi = media_directory(entity)
133
- FileUtils.makedirs mpi unless Dir.exists?(mpi)
134
- Dir.chdir mpi do
135
- yield
136
- end
163
+ FileUtils.makedirs mpi unless Dir.exist?(mpi)
164
+ Dir.chdir(mpi) { yield }
137
165
  end
138
166
 
139
- def odata_delete(request:, entity:)
140
- Dir.chdir(abs_klass_dir(entity)) do
167
+ def odata_delete(entity:)
168
+ Dir.chdir(@abs_klass_dir) do
141
169
  in_media_directory(entity) do
142
- Dir.glob('*').each { |oldf| File.delete(oldf) if File.file?(oldf) }
170
+ Dir.glob('*').sort.each { |oldf| File.delete(oldf) if File.file?(oldf) }
143
171
  end
144
- end
172
+ end
145
173
  end
146
-
174
+
175
+ # Here as well, MVP implementation
176
+ # def replace_file(data:, filename:, entity:)
177
+ # Dir.chdir(abs_klass_dir(entity)) do
178
+ # in_media_directory(entity) do
179
+ # Dir.glob('*').each { |oldf| File.delete(oldf) if File.file?(oldf) }
180
+ # File.open(filename, 'wb') { |f| IO.copy_stream(data, f) }
181
+ # end
182
+ # end
183
+ # end
147
184
  # Here as well, MVP implementation
148
185
  def replace_file(data:, filename:, entity:)
149
- Dir.chdir(abs_klass_dir(entity)) do
186
+ Dir.chdir(@abs_klass_dir) do
150
187
  in_media_directory(entity) do
151
- Dir.glob('*').each { |oldf| File.delete(oldf) if File.file?(oldf) }
188
+ version = nil
189
+ Dir.glob('*').sort.each do |oldf|
190
+ version = oldf
191
+ File.delete(oldf)
192
+ end
193
+ filename = (version.to_i + 1).to_s
152
194
  File.open(filename, 'wb') { |f| IO.copy_stream(data, f) }
153
195
  end
154
196
  end
155
197
  end
156
-
157
-
158
198
  end
159
-
160
199
  end
161
200
 
162
201
  # special handling for media entity
163
202
  module EntityClassMedia
164
203
  attr_reader :media_handler
165
204
  attr_reader :slug_field
166
-
205
+
167
206
  # API method for defining the media handler
168
207
  # eg.
169
208
  # publish_media_model photos do
170
- # use OData::Media::Static, :root => '/media_root'
209
+ # use Safrano::Media::Static, :root => '/media_root'
171
210
  # end
172
211
 
173
212
  def set_default_media_handler
174
- @media_handler = OData::Media::Static.new
213
+ @media_handler = Safrano::Media::Static.new(mediaklass: self)
175
214
  end
176
215
 
177
- def use(klass, *args)
178
- @media_handler = klass.new(*args)
216
+ def finalize_media
217
+ @media_handler.finalize
218
+ end
219
+
220
+ def use(klass, args)
221
+ args[:mediaklass] = self
222
+ @media_handler = klass.new(**args)
223
+ @media_handler.create_abs_class_dir
179
224
  end
180
225
 
181
226
  # API method for setting the model field mapped to SLUG on upload
182
227
  def slug(inp)
183
- @slug_field = inp
228
+ @slug_field = inp
184
229
  end
185
-
230
+
186
231
  def api_check_media_fields
187
- unless self.db_schema.has_key?(:content_type)
188
- raise OData::API::MediaModelError, self
189
- end
190
- # unless self.db_schema.has_key?(:media_src)
191
- # raise OData::API::MediaModelError, self
192
- # end
232
+ raise(Safrano::API::MediaModelError, self) unless db_schema.key?(:content_type)
193
233
  end
194
234
 
195
- # END API methods
235
+ # END API methods
196
236
 
197
237
  def new_media_entity(mimetype:)
198
- nh = {}
199
- nh['content_type'] = mimetype
238
+ nh = { 'content_type' => mimetype }
200
239
  new_from_hson_h(nh)
201
240
  end
202
241
 
@@ -209,11 +248,11 @@ module OData
209
248
  # NOTE: we will implement this first in a MVP way. There will be plenty of
210
249
  # potential future enhancements (performance, scallability, flexibility... with added complexity)
211
250
  # 4. create relation to parent if needed
212
- def odata_create_entity_and_relation(req, assoc, parent)
251
+ def odata_create_entity_and_relation(req, assoc = nil, parent = nil)
213
252
  req.with_media_data do |data, mimetype, filename|
214
253
  ## future enhancement: validate allowed mimetypes ?
215
254
  # if (invalid = invalid_media_mimetype(mimetype))
216
- # ::OData::Request::ON_CGST_ERROR.call(req)
255
+ # ::Safrano::Request::ON_CGST_ERROR.call(req)
217
256
  # return [422, {}, ['Invalid mime type: ', invalid.to_s]]
218
257
  # end
219
258
 
@@ -223,11 +262,11 @@ module OData
223
262
 
224
263
  if slug_field
225
264
 
226
- new_entity.set_fields({ slug_field => filename},
227
- data_fields,
228
- missing: :skip)
265
+ new_entity.set_fields({ slug_field => filename },
266
+ data_fields,
267
+ missing: :skip)
229
268
  end
230
-
269
+
231
270
  # to_one rels are create with FK data set on the parent entity
232
271
  if parent
233
272
  odata_create_save_entity_and_rel(req, new_entity, assoc, parent)
@@ -239,9 +278,18 @@ module OData
239
278
  req.register_content_id_ref(new_entity)
240
279
  new_entity.copy_request_infos(req)
241
280
 
242
- media_handler.save_file(data: data, entity: new_entity, filename: filename)
281
+ # call before_create_media hook
282
+ new_entity.before_create_media if new_entity.respond_to? :before_create_media
283
+
284
+ media_handler.save_file(data: data,
285
+ entity: new_entity,
286
+ filename: filename)
243
287
 
244
- [201, CT_JSON, new_entity.to_odata_post_json(service: req.service)]
288
+ # call after_create_media hook
289
+ new_entity.after_create_media if new_entity.respond_to? :after_create_media
290
+
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)]
245
293
  else # TODO: other formats
246
294
  415
247
295
  end
@@ -249,3 +297,26 @@ module OData
249
297
  end
250
298
  end
251
299
  end
300
+
301
+ # deprecated
302
+ # REMOVE 0.6
303
+ module OData
304
+ module Media
305
+ class Static < ::Safrano::Media::Static
306
+ def initialize(root: nil, mediaklass:)
307
+ ::Safrano::Deprecation.deprecate('OData::Media::Static',
308
+ 'Use Safrano::Media::Static instead')
309
+
310
+ super
311
+ end
312
+ end
313
+ class StaticTree < ::Safrano::Media::StaticTree
314
+ def initialize(root: nil, mediaklass:)
315
+ ::Safrano::Deprecation.deprecate('OData::Media::StaticTree',
316
+ 'Use Safrano::Media::StaticTree instead')
317
+
318
+ super
319
+ end
320
+ end
321
+ end
322
+ end