safrano 0.3.4 → 0.4.0

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: a4f2a075aba57f7986bf9727748730eb17f4302da16a3193c66f260971b5e1c6
4
- data.tar.gz: c11e78ef5ed8b63332129dfc0fc23829ba9494697ce7fc65334e34ceaf68b90c
3
+ metadata.gz: 0a16c6b649ec630fbada504c11039b017b4055c7038c2aa126e7c6b55dd768ff
4
+ data.tar.gz: 03b387f824a5f2e4d53c3e16dd9b013ca8d3c308bc06e13cc0bf09bf54f4f3eb
5
5
  SHA512:
6
- metadata.gz: cdc676ba941a7170ff9ca0076614a62e495c1c8f39959a7a64e7a747a94e3676b5ee35eaa10fd832f116f10279e40ae7929b1ac5109db69bc1b7eebb20d08730
7
- data.tar.gz: 03ec76d115241fc7c22337b15228cf294f5e5a15366ba7869fd05f9e1feaf7ce6ac278faa9ae0b34b013ed0c22b04babc5e14a67463d811dcb1edfc58640438f
6
+ metadata.gz: d40563e3e2ad2db813954318aaf5d0f3097f71d9210af9434524cc7e8e25ba81b37694e0fe5fb7d7f9c662679a3b5c8425d11e771d8ba0fe42162598d4f81f65
7
+ data.tar.gz: 8dc6cfea884a95c2841e0646d0051dcf4926c3fad85734978e8ee2bc98ea7dbae5fd3718cf6a3a8b49f66c2a3566c33c70c64fd8babde8c4c66a1cbe2eb46929
@@ -1,5 +1,5 @@
1
1
  require 'json'
2
- require_relative '../safrano_core.rb'
2
+ require_relative '../safrano/core.rb'
3
3
  require_relative './entity.rb'
4
4
 
5
5
  module OData
@@ -1,5 +1,5 @@
1
- require 'rack_app.rb'
2
- require 'safrano_core.rb'
1
+ require_relative '../safrano/rack_app.rb'
2
+ require_relative '../safrano/core.rb'
3
3
  require 'rack/body_proxy'
4
4
  require_relative './common_logger.rb'
5
5
 
@@ -59,7 +59,7 @@ module OData
59
59
  # TODO: test ?
60
60
  if (logga = @full_req.env['safrano.logger_mw'])
61
61
  logga.batch_log(env, status, header, began_at)
62
- # TODO check why/if we need Rack::Utils::HeaderHash.new(header)
62
+ # TODO check why/if we need Rack::Utils::HeaderHash.new(header)
63
63
  # and Rack::BodyProxy.new(body) ?
64
64
  end
65
65
  [status, header, body]
@@ -4,12 +4,12 @@
4
4
 
5
5
  require 'json'
6
6
  require 'rexml/document'
7
- require 'safrano_core.rb'
8
- require 'odata/error.rb'
9
- require 'odata/collection_filter.rb'
10
- require 'odata/collection_order.rb'
11
- require 'odata/url_parameters.rb'
12
- require 'odata/collection_media.rb'
7
+ require_relative '../safrano/core.rb'
8
+ require_relative 'error.rb'
9
+ require_relative 'collection_filter.rb'
10
+ require_relative 'collection_order.rb'
11
+ require_relative 'url_parameters.rb'
12
+ require_relative 'collection_media.rb'
13
13
 
14
14
  # small helper method
15
15
  # http://stackoverflow.com/
@@ -53,6 +53,13 @@ module OData
53
53
  attr_reader :nav_entity_attribs
54
54
  attr_reader :data_fields
55
55
  attr_reader :inlinecount
56
+
57
+ # Sequel associations pointing to this model. Sequel provides association
58
+ # reflection information on the "from" side. But in some cases
59
+ # we will need the reverted way
60
+ # finally not needed and not used yet
61
+ # attr_accessor :assocs_to
62
+
56
63
  # set to parent entity in case the collection is a nav.collection
57
64
  # nil otherwise
58
65
  attr_reader :nav_parent
@@ -288,7 +295,11 @@ module OData
288
295
 
289
296
  # add metadata xml to the passed REXML schema object
290
297
  def add_metadata_rexml(schema)
291
- enty = schema.add_element('EntityType', 'Name' => to_s)
298
+ enty = if @media_handler
299
+ schema.add_element('EntityType', 'Name' => to_s, 'HasStream' => 'true' )
300
+ else
301
+ schema.add_element('EntityType', 'Name' => to_s)
302
+ end
292
303
  # with their properties
293
304
  db_schema.each do |pnam, prop|
294
305
  if prop[:primary_key] == true
@@ -304,25 +315,37 @@ module OData
304
315
  end
305
316
 
306
317
  # metadata REXML data for a single Nav attribute
307
- def metadata_nav_rexml_attribs(assoc, cmap, relman, xnamespace)
318
+ def metadata_nav_rexml_attribs(assoc, to_klass, relman, xnamespace)
308
319
  from = type_name
309
- to = cmap[assoc.to_s].type_name
320
+ to = to_klass.type_name
310
321
  relman.get_metadata_xml_attribs(from,
311
322
  to,
312
- association_reflection(assoc)[:type],
313
- xnamespace)
323
+ association_reflection(assoc.to_sym)[:type],
324
+ xnamespace,
325
+ assoc)
326
+
314
327
  end
315
328
 
316
329
  # and their Nav attributes == Sequel Model association
317
- def add_metadata_navs_rexml(schema_enty, cmap, relman, xnamespace)
318
- associations.each do |assoc|
319
- # associated objects need to be in the map...
320
- next unless cmap[assoc.to_s]
321
-
322
- nattrs = metadata_nav_rexml_attribs(assoc, cmap, relman, xnamespace)
323
-
324
- schema_enty.add_element('NavigationProperty', nattrs)
325
- end
330
+ def add_metadata_navs_rexml(schema_enty, relman, xnamespace)
331
+
332
+ @nav_entity_attribs.each{|ne,klass|
333
+ nattr = metadata_nav_rexml_attribs(ne,
334
+ klass,
335
+ relman,
336
+ xnamespace)
337
+ schema_enty.add_element('NavigationProperty', nattr)
338
+ } if @nav_entity_attribs
339
+
340
+
341
+ @nav_collection_attribs.each{|nc,klass|
342
+ nattr = metadata_nav_rexml_attribs(nc,
343
+ klass,
344
+ relman,
345
+ xnamespace)
346
+ schema_enty.add_element('NavigationProperty', nattr)
347
+ } if @nav_collection_attribs
348
+
326
349
  end
327
350
 
328
351
  D = 'd'.freeze
@@ -351,6 +374,7 @@ module OData
351
374
  # We need to base this on the Sequel rels, or extend them
352
375
  def add_nav_prop_collection(assoc_symb, attr_name_str = nil)
353
376
  @nav_collection_attribs = (@nav_collection_attribs || {})
377
+ # @assocs_to = ( @assocs_to || [] )
354
378
  # DONE: Error handling. This requires that associations
355
379
  # have been properly defined with Sequel before
356
380
  assoc = all_association_reflections.find do |a|
@@ -368,6 +392,7 @@ module OData
368
392
 
369
393
  def add_nav_prop_single(assoc_symb, attr_name_str = nil)
370
394
  @nav_entity_attribs = (@nav_entity_attribs || {})
395
+ # @assocs_to = ( @assocs_to || [])
371
396
  # DONE: Error handling. This requires that associations
372
397
  # have been properly defined with Sequel before
373
398
  assoc = all_association_reflections.find do |a|
@@ -381,12 +406,22 @@ module OData
381
406
  lattr_name_str = (attr_name_str || assoc_symb.to_s)
382
407
  @nav_entity_attribs[lattr_name_str] = attr_class
383
408
  @nav_entity_url_regexp = @nav_entity_attribs.keys.join('|')
409
+ # attr_class.assocs_to = ( attr_class.assocs_to || [] )
410
+ # attr_class.assocs_to << assoc
384
411
  end
385
412
 
386
413
  # old names...
387
414
  # alias_method :add_nav_prop_collection, :addNavCollectionAttrib
388
415
  # alias_method :add_nav_prop_single, :addNavEntityAttrib
389
416
 
417
+ def finalize_publishing
418
+ # finalize media handler
419
+ @media_handler.register(self) if @media_handler
420
+
421
+ # and finally build the path list
422
+ build_attribute_path_list
423
+ end
424
+
390
425
  def prepare_pk
391
426
  if primary_key.is_a? Array
392
427
  @pk_names = []
@@ -1,4 +1,5 @@
1
1
  require 'rack'
2
+ require 'fileutils'
2
3
  require_relative './navigation_attribute.rb'
3
4
 
4
5
  module OData
@@ -9,49 +10,84 @@ module OData
9
10
 
10
11
  # Simple static File/Directory based media store handler
11
12
  # similar to Rack::Static
13
+ # with a flat directory structure
12
14
  class Static < Handler
15
+
13
16
  def initialize(root: nil)
14
17
  @root = File.absolute_path(root || Dir.pwd)
15
18
  @file_server = ::Rack::File.new(@root)
16
19
  end
17
20
 
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)
25
+ end
26
+
18
27
  # minimal working implementation...
19
28
  # Note: @file_server works relative to @root directory
20
29
  def odata_get(request:, entity:)
21
30
  media_env = request.env.dup
22
- relpath = Dir.chdir(abs_path(entity)) do
23
- # simple design: one file per directory, and the directory
24
- # contains the media entity-id --> implicit link between the media
25
- # entity
26
- filename = Dir.glob('*').first
27
-
28
- File.join(path(entity), filename)
29
- end
30
- media_env['PATH_INFO'] = relpath
31
+ media_env['PATH_INFO'] = filename(entity)
31
32
  @file_server.call(media_env)
32
33
  end
33
34
 
34
35
  # TODO perf: this can be precalculated and cached on MediaModelKlass level
35
36
  # and passed as argument to save_file
37
+ # eg. /@root/Photo
36
38
  def abs_klass_dir(entity)
37
39
  File.absolute_path(entity.klass_dir, @root)
38
40
  end
39
41
 
42
+ # this is relative to @root
43
+ # eg. Photo/1
44
+ def media_path(entity)
45
+ File.join(entity.klass_dir, media_directory(entity))
46
+ end
47
+
48
+ # relative to @root
49
+ # eg Photo/1/pommes-topaz.jpg
50
+ def filename(entity)
51
+ Dir.chdir(abs_path(entity)) do
52
+ # simple design: one file per directory, and the directory
53
+ # contains the media entity-id --> implicit link between the media
54
+ # entity
55
+ File.join(media_path(entity), Dir.glob('*').first)
56
+ end
57
+ end
58
+
59
+ # /@root/Photo/1
40
60
  def abs_path(entity)
41
- File.absolute_path(path(entity), @root)
61
+ File.absolute_path(media_path(entity), @root)
42
62
  end
43
63
 
44
- # this is relative to @root
45
- def path(entity)
46
- File.join(entity.klass_dir, entity.media_path_id)
64
+ # this is relative to abs_klass_dir(entity) eg to /@root/Photo
65
+ # simplest implementation is media_directory = entity.media_path_id
66
+ # --> we get a 1 level depth flat directory structure
67
+ def media_directory(entity)
68
+ entity.media_path_id
69
+ end
70
+
71
+ def in_media_directory(entity)
72
+ mpi = media_directory(entity)
73
+ Dir.mkdir mpi unless Dir.exists?(mpi)
74
+ Dir.chdir mpi do
75
+ yield
76
+ end
77
+ end
78
+
79
+ def odata_delete(request:, entity:)
80
+ Dir.chdir(abs_klass_dir(entity)) do
81
+ in_media_directory(entity) do
82
+ Dir.glob('*').each { |oldf| File.delete(oldf) }
83
+ end
84
+ end
47
85
  end
48
86
 
49
87
  # Here as well, MVP implementation
50
88
  def save_file(data:, filename:, entity:)
51
- mpi = entity.media_path_id
52
89
  Dir.chdir(abs_klass_dir(entity)) do
53
- Dir.mkdir mpi unless Dir.exists?(mpi)
54
- Dir.chdir mpi do
90
+ in_media_directory(entity) do
55
91
  File.open(filename, 'wb') { |f| IO.copy_stream(data, f) }
56
92
  end
57
93
  end
@@ -59,22 +95,75 @@ module OData
59
95
 
60
96
  # Here as well, MVP implementation
61
97
  def replace_file(data:, filename:, entity:)
62
- mpi = entity.media_path_id
63
98
  Dir.chdir(abs_klass_dir(entity)) do
64
- Dir.mkdir mpi unless Dir.exists?(mpi)
65
- Dir.chdir mpi do
99
+ in_media_directory(entity) do
66
100
  Dir.glob('*').each { |oldf| File.delete(oldf) }
67
101
  File.open(filename, 'wb') { |f| IO.copy_stream(data, f) }
68
102
  end
69
103
  end
70
104
  end
71
105
  end
106
+ # Simple static File/Directory based media store handler
107
+ # similar to Rack::Static
108
+ # with directory Tree structure
109
+
110
+ 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)
116
+ end
117
+
118
+ # this is relative to abs_klass_dir(entity) eg to /@root/Photo
119
+ # tree-structure
120
+ # media_path_ids = 1 --> 1
121
+ # media_path_ids = 15 --> 1/5
122
+ # media_path_ids = 555 --> 5/5/5
123
+ # media_path_ids = 5,5,5 --> 5/00/5/00/5
124
+ # media_path_ids = 5,00,5 --> 5/00/0/0/00/5
125
+ # media_path_ids = 5,xyz,5 --> 5/00/x/y/z/00/5
126
+ def media_directory(entity)
127
+ StaticTree.path_builder(entity.media_path_ids)
128
+ # entity.media_path_ids.map{|id| id.to_s.chars.join('/')}.join(@sep)
129
+ end
130
+
131
+ def in_media_directory(entity)
132
+ mpi = media_directory(entity)
133
+ FileUtils.makedirs mpi unless Dir.exists?(mpi)
134
+ Dir.chdir mpi do
135
+ yield
136
+ end
137
+ end
138
+
139
+ def odata_delete(request:, entity:)
140
+ Dir.chdir(abs_klass_dir(entity)) do
141
+ in_media_directory(entity) do
142
+ Dir.glob('*').each { |oldf| File.delete(oldf) if File.file?(oldf) }
143
+ end
144
+ end
145
+ end
146
+
147
+ # Here as well, MVP implementation
148
+ def replace_file(data:, filename:, entity:)
149
+ Dir.chdir(abs_klass_dir(entity)) do
150
+ in_media_directory(entity) do
151
+ Dir.glob('*').each { |oldf| File.delete(oldf) if File.file?(oldf) }
152
+ File.open(filename, 'wb') { |f| IO.copy_stream(data, f) }
153
+ end
154
+ end
155
+ end
156
+
157
+
158
+ end
159
+
72
160
  end
73
161
 
74
162
  # special handling for media entity
75
163
  module EntityClassMedia
76
164
  attr_reader :media_handler
77
-
165
+ attr_reader :slug_field
166
+
78
167
  # API method for defining the media handler
79
168
  # eg.
80
169
  # publish_media_model photos do
@@ -89,6 +178,11 @@ module OData
89
178
  @media_handler = klass.new(*args)
90
179
  end
91
180
 
181
+ # API method for setting the model field mapped to SLUG on upload
182
+ def slug(inp)
183
+ @slug_field = inp
184
+ end
185
+
92
186
  def api_check_media_fields
93
187
  unless self.db_schema.has_key?(:content_type)
94
188
  raise OData::API::MediaModelError, self
@@ -98,6 +192,8 @@ module OData
98
192
  # end
99
193
  end
100
194
 
195
+ # END API methods
196
+
101
197
  def new_media_entity(mimetype:)
102
198
  nh = {}
103
199
  nh['content_type'] = mimetype
@@ -125,6 +221,13 @@ module OData
125
221
 
126
222
  new_entity = new_media_entity(mimetype: mimetype)
127
223
 
224
+ if slug_field
225
+
226
+ new_entity.set_fields({ slug_field => filename},
227
+ data_fields,
228
+ missing: :skip)
229
+ end
230
+
128
231
  # to_one rels are create with FK data set on the parent entity
129
232
  if parent
130
233
  odata_create_save_entity_and_rel(req, new_entity, assoc, parent)
@@ -5,21 +5,46 @@ module Rack
5
5
  super
6
6
  end
7
7
 
8
+ # Handle https://github.com/rack/rack/pull/1526
9
+ # new in Rack 2.2.2 : Format has now 11 placeholders instead of 10
10
+
11
+ MSG_FUNC = if (FORMAT.count('%') == 10)
12
+ lambda {|env,length,status,began_at|
13
+ FORMAT % [
14
+ env['HTTP_X_FORWARDED_FOR'] || env["REMOTE_ADDR"] || "-",
15
+ env["REMOTE_USER"] || "-",
16
+ Time.now.strftime("%d/%b/%Y:%H:%M:%S %z"),
17
+ env[REQUEST_METHOD],
18
+ env[SCRIPT_NAME] + env[PATH_INFO],
19
+ env[QUERY_STRING].empty? ? "" : "?#{env[QUERY_STRING]}",
20
+ env[SERVER_PROTOCOL],
21
+ status.to_s[0..3],
22
+ length,
23
+ Utils.clock_time - began_at
24
+ ]
25
+ }
26
+ elsif (FORMAT.count('%') == 11)
27
+ lambda {|env,length,status,began_at|
28
+ FORMAT % [
29
+ env['HTTP_X_FORWARDED_FOR'] || env["REMOTE_ADDR"] || "-",
30
+ env["REMOTE_USER"] || "-",
31
+ Time.now.strftime("%d/%b/%Y:%H:%M:%S %z"),
32
+ env[REQUEST_METHOD],
33
+ env[SCRIPT_NAME],
34
+ env[PATH_INFO],
35
+ env[QUERY_STRING].empty? ? "" : "?#{env[QUERY_STRING]}",
36
+ env[SERVER_PROTOCOL],
37
+ status.to_s[0..3],
38
+ length,
39
+ Utils.clock_time - began_at
40
+ ]
41
+ }
42
+ end
43
+
8
44
  def batch_log(env, status, header, began_at)
9
45
  length = extract_content_length(header)
10
46
 
11
- msg = FORMAT % [
12
- env['HTTP_X_FORWARDED_FOR'] || env["REMOTE_ADDR"] || "-",
13
- env["REMOTE_USER"] || "-",
14
- Time.now.strftime("%d/%b/%Y:%H:%M:%S %z"),
15
- env[REQUEST_METHOD],
16
- env[PATH_INFO],
17
- env[QUERY_STRING].empty? ? "" : "?#{env[QUERY_STRING]}",
18
- env[SERVER_PROTOCOL],
19
- status.to_s[0..3],
20
- length,
21
- Utils.clock_time - began_at
22
- ]
47
+ msg = MSG_FUNC.call(env, length, status, began_at)
23
48
 
24
49
  logger = @logger || env[RACK_ERRORS]
25
50
  # Standard library logger doesn't support write but it supports << which actually
@@ -2,6 +2,7 @@ require 'json'
2
2
  require 'rexml/document'
3
3
  require 'safrano.rb'
4
4
  require 'odata/collection.rb' # required for self.class.entity_type_name ??
5
+ require_relative 'navigation_attribute'
5
6
 
6
7
  module OData
7
8
  # this will be mixed in the Model classes (subclasses of Sequel Model)
@@ -9,6 +10,8 @@ module OData
9
10
  attr_reader :params
10
11
  attr_reader :uribase
11
12
 
13
+ include EntityBase::NavigationInfo
14
+
12
15
  # methods related to transitions to next state (cf. walker)
13
16
  module Transitions
14
17
  def allowed_transitions
@@ -98,11 +101,18 @@ module OData
98
101
  def to_odata_json(service:)
99
102
  innerj = service.get_entity_odata_h(entity: self,
100
103
  expand: @params['$expand'],
101
- # links: @do_links,
102
104
  uribase: @uribase).to_json
103
105
  "#{DJopen}#{innerj}#{DJclose}"
104
106
  end
105
107
 
108
+ # Json formatter for a single entity reached by navigation $links
109
+ def to_odata_onelink_json(service:)
110
+ innerj = service.get_entity_odata_link_h(entity: self,
111
+ uribase: @uribase).to_json
112
+ "#{DJopen}#{innerj}#{DJclose}"
113
+ end
114
+
115
+
106
116
  # needed for proper datetime output
107
117
  # TODO design/performance
108
118
  def casted_values
@@ -144,21 +154,38 @@ module OData
144
154
  if req.walker.media_value
145
155
  odata_media_value_get(req)
146
156
  elsif req.accept?(APPJSON)
147
- [200, CT_JSON, [to_odata_json(service: req.service)]]
157
+ if req.walker.do_links
158
+ [200, CT_JSON, [to_odata_onelink_json(service: req.service)]]
159
+ else
160
+ [200, CT_JSON, [to_odata_json(service: req.service)]]
161
+ end
148
162
  else # TODO: other formats
149
163
  415
150
164
  end
151
165
  end
152
-
153
- def odata_delete(req)
154
- if req.accept?(APPJSON)
155
- delete
156
- [200, CT_JSON, [{ 'd' => req.service.get_emptycoll_odata_h }.to_json]]
157
- else # TODO: other formats
158
- 415
166
+
167
+ DELETE_REL_AND_ENTY = lambda do |entity, assoc, parent|
168
+ OData.remove_nav_relation(entity, assoc, parent)
169
+ entity.destroy(transaction: false)
170
+ end
171
+
172
+ def odata_delete_relation_and_entity(req, assoc, parent)
173
+ if parent
174
+ if req.in_changeset
175
+ # in-changeset requests get their own transaction
176
+ DELETE_REL_AND_ENTY.call(self, assoc, parent)
177
+ else
178
+ db.transaction do
179
+ DELETE_REL_AND_ENTY.call(self, assoc, parent)
180
+ end
181
+ end
182
+ else
183
+ destroy(transaction: false)
159
184
  end
185
+ rescue StandardError => e
186
+ raise SequelAdapterError.new(e)
160
187
  end
161
-
188
+
162
189
  # TODO: differentiate between POST/PUT/PATCH/MERGE
163
190
  def odata_post(req)
164
191
  data = JSON.parse(req.body.read)
@@ -253,6 +280,11 @@ module OData
253
280
  y.each { |enty| yield enty }
254
281
  end
255
282
 
283
+ # TODO design... this is not DRY
284
+ def slug_field
285
+ superclass.slug_field
286
+ end
287
+
256
288
  def type_name
257
289
  superclass.type_name
258
290
  end
@@ -288,7 +320,7 @@ module OData
288
320
  extend NavigationRedefinitions
289
321
  end
290
322
  end
291
-
323
+
292
324
  # GetRelatedEntity that returns an single related Entity
293
325
  # (...to_one relationship )
294
326
  def get_related_entity(childattrib)
@@ -301,9 +333,10 @@ module OData
301
333
  # then we return a Nil... wrapper object. This object then
302
334
  # allows to receive a POST operation that would actually create the nav attribute entity
303
335
 
304
- ret = method(childattrib.to_sym).call ||
305
- OData::NilNavigationAttribute.new(self, childattrib)
306
-
336
+ ret = method(childattrib.to_sym).call || OData::NilNavigationAttribute.new
337
+
338
+ ret.set_relation_info(self, childattrib)
339
+
307
340
  ret
308
341
  end
309
342
  end
@@ -323,6 +356,20 @@ module OData
323
356
  values.dup
324
357
  end
325
358
 
359
+ def odata_delete(req)
360
+ if req.accept?(APPJSON)
361
+ # delete
362
+ begin
363
+ odata_delete_relation_and_entity(req, @navattr_reflection, @nav_parent)
364
+ [200, CT_JSON, [{ 'd' => req.service.get_emptycoll_odata_h }.to_json]]
365
+ rescue SequelAdapterError => e
366
+ BadRequestSequelAdapterError.new(e).odata_get(req)
367
+ end
368
+ else # TODO: other formats
369
+ 415
370
+ end
371
+ end
372
+
326
373
  # in case of a non media entity, we have to return an error on $value request
327
374
  def odata_media_value_get(req)
328
375
  return BadRequestNonMediaValue.odata_get
@@ -364,6 +411,23 @@ module OData
364
411
  ret
365
412
  end
366
413
 
414
+ def odata_delete(req)
415
+ if req.accept?(APPJSON)
416
+ # delete the MR
417
+ # delegate to the media handler on collection(ie class) level
418
+ # TODO error handling
419
+
420
+
421
+ self.class.media_handler.odata_delete(request: req, entity: self)
422
+ # delete the relation(s) to parent(s) (if any) and then entity
423
+ odata_delete_relation_and_entity(req, @navattr_reflection, @nav_parent)
424
+ # result
425
+ [200, CT_JSON, [{ 'd' => req.service.get_emptycoll_odata_h }.to_json]]
426
+ else # TODO: other formats
427
+ 415
428
+ end
429
+ end
430
+
367
431
  # real implementation for returning $value for a media entity
368
432
  def odata_media_value_get(req)
369
433
  # delegate to the media handler on collection(ie class) level
@@ -399,6 +463,9 @@ module OData
399
463
  def media_path_id
400
464
  pk.to_s
401
465
  end
466
+ def media_path_ids
467
+ [pk]
468
+ end
402
469
  end
403
470
 
404
471
  # for multiple key
@@ -412,6 +479,10 @@ module OData
412
479
  def media_path_id
413
480
  self.pk_hash.values.join('_')
414
481
  end
482
+
483
+ def media_path_ids
484
+ self.pk_hash.values
485
+ end
415
486
  end
416
487
  end
417
488
  # end of Module OData
@@ -31,8 +31,8 @@ module OData
31
31
  end
32
32
  end
33
33
 
34
- # base module for HTTP errors
35
- module Error
34
+ # base module for HTTP errors, when used as a Error Class
35
+ module ErrorClass
36
36
  def odata_get(req)
37
37
  if req.accept?(APPJSON)
38
38
  [const_get(:HTTP_CODE), CT_JSON,
@@ -42,9 +42,22 @@ module OData
42
42
  end
43
43
  end
44
44
  end
45
+
46
+ # base module for HTTP errors, when used as an Error instance
47
+ module ErrorInstance
48
+ def odata_get(req)
49
+ if req.accept?(APPJSON)
50
+ [self.class.const_get(:HTTP_CODE), CT_JSON,
51
+ { 'odata.error' => { 'code' => '', 'message' => @msg } }.to_json]
52
+ else
53
+ [self.class.const_get(:HTTP_CODE), CT_TEXT, @msg]
54
+ end
55
+ end
56
+ end
57
+
45
58
  # http Bad Req.
46
59
  class BadRequestError
47
- extend Error
60
+ extend ErrorClass
48
61
  HTTP_CODE = 400
49
62
  @msg = 'Bad Request Error'
50
63
  end
@@ -59,7 +72,13 @@ module OData
59
72
  HTTP_CODE = 400
60
73
  @msg = 'Bad Request: $value request for a non-media entity'
61
74
  end
62
-
75
+ class BadRequestSequelAdapterError < BadRequestError
76
+ include ErrorInstance
77
+ def initialize(err)
78
+ @msg = err.inner.message
79
+ end
80
+ end
81
+
63
82
  # for Syntax error in Filtering
64
83
  class BadRequestFilterParseError < BadRequestError
65
84
  HTTP_CODE = 400
@@ -72,36 +91,36 @@ module OData
72
91
  end
73
92
  # http not found
74
93
  class ErrorNotFound
75
- extend Error
94
+ extend ErrorClass
76
95
  HTTP_CODE = 404
77
96
  @msg = 'The requested ressource was not found'
78
97
  end
79
98
  # Transition error (Safrano specific)
80
99
  class ServerTransitionError
81
- extend Error
100
+ extend ErrorClass
82
101
  HTTP_CODE = 500
83
102
  @msg = 'Server error: Segment could not be parsed'
84
103
  end
85
104
  # generic http 500 server err
86
105
  class ServerError
87
- extend Error
106
+ extend ErrorClass
88
107
  HTTP_CODE = 500
89
108
  @msg = 'Server error'
90
109
  end
91
110
  # not implemented (Safrano specific)
92
111
  class NotImplementedError
93
- extend Error
112
+ extend ErrorClass
94
113
  HTTP_CODE = 501
95
114
  end
96
115
  # batch not implemented (Safrano specific)
97
116
  class BatchNotImplementedError
98
- extend Error
117
+ extend ErrorClass
99
118
  HTTP_CODE = 501
100
119
  @msg = 'Not implemented: OData batch'
101
120
  end
102
121
  # error in filter parsing (Safrano specific)
103
122
  class FilterParseError < BadRequestError
104
- extend Error
123
+ extend ErrorClass
105
124
  HTTP_CODE = 400
106
125
  end
107
126
  end
@@ -1,4 +1,10 @@
1
1
  module OData
2
+ class SequelAdapterError < StandardError
3
+ attr_reader :inner
4
+ def initialize(err)
5
+ @inner = err
6
+ end
7
+ end
2
8
  module Filter
3
9
  class Parser
4
10
  # Parser errors
@@ -1,8 +1,30 @@
1
1
  require 'json'
2
- require_relative '../safrano_core.rb'
2
+ require_relative '../safrano/core.rb'
3
3
  require_relative './entity.rb'
4
4
 
5
5
  module OData
6
+
7
+ # remove the relation between entity and parent by clearing
8
+ # the FK field(s) (if allowed)
9
+ def OData.remove_nav_relation(entity, assoc, parent)
10
+ return unless assoc
11
+
12
+ case assoc[:type]
13
+ when :one_to_many, :one_to_one
14
+ when :many_to_one
15
+ # removes/clear the FK values in parent
16
+ # thus deleting the "link" between the entity and the parent
17
+ # Note: This is called if we have to delete the child--> can only be
18
+ # done after removing the FK in parent (if allowed!)
19
+ lks = [assoc[:key]].flatten
20
+ lks.each{|lk|
21
+ parent.set(lk => nil )
22
+ parent.save(transaction: false)
23
+ }
24
+
25
+ end
26
+ end
27
+
6
28
  # link newly created entities(child) to an existing parent
7
29
  # by following the association_reflection rules
8
30
  def OData.create_nav_relation(child, assoc, parent)
@@ -55,18 +77,24 @@ module OData
55
77
  end
56
78
  end
57
79
 
80
+ module EntityBase
81
+ module NavigationInfo
82
+ attr_reader :nav_parent
83
+ attr_reader :navattr_reflection
84
+ attr_reader :nav_name
85
+ def set_relation_info(parent,name)
86
+ @nav_parent = parent
87
+ @nav_name = name
88
+ @navattr_reflection = parent.class.association_reflections[name.to_sym]
89
+ @nav_klass = @navattr_reflection[:class_name].constantize
90
+ end
91
+ end
92
+ end
93
+
58
94
  # Represents a named but nil-valued navigation-attribute of an Entity
59
95
  # (usually resulting from a NULL FK db value)
60
96
  class NilNavigationAttribute
61
- attr_reader :name
62
- attr_reader :parent
63
- def initialize(parent, name)
64
- @parent = parent
65
- @name = name
66
- @navattr_reflection = parent.class.association_reflections[name.to_sym]
67
- @klass = @navattr_reflection[:class_name].constantize
68
- end
69
-
97
+ include EntityBase::NavigationInfo
70
98
  def odata_get(req)
71
99
  if req.walker.media_value
72
100
  OData::ErrorNotFound.odata_get
@@ -80,13 +108,20 @@ module OData
80
108
  # create the nav. entity
81
109
  def odata_post(req)
82
110
  # delegate to the class method
83
- @klass.odata_create_entity_and_relation(req, @navattr_reflection, @parent)
111
+ @nav_klass.odata_create_entity_and_relation(req,
112
+ @navattr_reflection,
113
+ @nav_parent)
84
114
  end
85
115
 
86
116
  # create the nav. entity
87
117
  def odata_put(req)
88
- # delegate to the class method
89
- @klass.odata_create_entity_and_relation(req, @navattr_reflection, @parent)
118
+ # if req.walker.raw_value
119
+ # delegate to the class method
120
+ @nav_klass.odata_create_entity_and_relation(req,
121
+ @navattr_reflection,
122
+ @nav_parent)
123
+ # else
124
+ # end
90
125
  end
91
126
 
92
127
  # empty output as OData json (v2)
@@ -96,7 +131,7 @@ module OData
96
131
 
97
132
  # for testing purpose (assert_equal ...)
98
133
  def ==(other)
99
- (@parent == other.parent) && (@name == other.name)
134
+ (@nav_parent == other.nav_parent) && (@nav_name == other.nav_name)
100
135
  end
101
136
 
102
137
  # methods related to transitions to next state (cf. walker)
@@ -85,7 +85,7 @@ module OData
85
85
  end
86
86
  end
87
87
 
88
- def get_metadata_xml_attribs(from, to, assoc_type, xnamespace)
88
+ def get_metadata_xml_attribs(from, to, assoc_type, xnamespace, attrname)
89
89
  rel = get([from, to])
90
90
  # use Sequel reflection to get multiplicity (will be used later
91
91
  # in 2. Associations below)
@@ -107,7 +107,7 @@ module OData
107
107
  # <NavigationProperty Name="Supplier"
108
108
  # Relationship="ODataDemo.Product_Supplier_Supplier_Products"
109
109
  # FromRole="Product_Supplier" ToRole="Supplier_Products"/>
110
- { 'Name' => to, 'Relationship' => "#{xnamespace}.#{rel.name}",
110
+ { 'Name' => attrname, 'Relationship' => "#{xnamespace}.#{rel.name}",
111
111
  'FromRole' => from, 'ToRole' => to }
112
112
  end
113
113
  end
@@ -1,19 +1,19 @@
1
- #!/usr/bin/env ruby
2
1
 
3
2
  require 'json'
4
3
  require 'rexml/document'
5
- require_relative './multipart.rb'
6
- require 'safrano_core.rb'
7
- require 'odata/entity.rb'
8
- require 'odata/attribute.rb'
9
- require 'odata/navigation_attribute.rb'
10
- require 'odata/collection.rb'
11
- require 'service.rb'
12
- require 'odata/walker.rb'
4
+ require_relative 'safrano/multipart.rb'
5
+ require_relative 'safrano/core.rb'
6
+ require_relative 'odata/entity.rb'
7
+ require_relative 'odata/attribute.rb'
8
+ require_relative 'odata/navigation_attribute.rb'
9
+ require_relative 'odata/collection.rb'
10
+ require_relative 'safrano/service.rb'
11
+ require_relative 'odata/walker.rb'
13
12
  require 'sequel'
14
- require_relative './sequel_join_by_paths.rb'
15
- require 'rack_app'
16
- require 'odata_rack_builder'
13
+ require_relative 'safrano/sequel_join_by_paths.rb'
14
+ require_relative 'safrano/rack_app'
15
+ require_relative 'safrano/odata_rack_builder'
16
+ require_relative 'safrano/version'
17
17
 
18
18
  # picked from activsupport; needed for ruby < 2.5
19
19
  # Destructively converts all keys using the +block+ operations.
@@ -30,3 +30,13 @@ class Hash
30
30
  transform_keys! { |key| key.to_sym rescue key }
31
31
  end
32
32
  end
33
+
34
+ # needed for ruby < 2.5
35
+ class Dir
36
+ def self.each_child(dir)
37
+ Dir.foreach(dir) {|x|
38
+ next if ( ( x == '.' ) or ( x == '..' ) )
39
+ yield x
40
+ }
41
+ end unless respond_to? :each_child
42
+ end
@@ -32,14 +32,15 @@ module OData
32
32
  # database-specific types.
33
33
  DB_TYPE_STRING_RGX = /\ACHAR\s*\(\d+\)\z/.freeze
34
34
 
35
+ # TODO... complete; used in $metadata
35
36
  def self.get_edm_type(db_type:)
36
- case db_type
37
+ case db_type.upcase
37
38
  when 'INTEGER'
38
39
  'Edm.Int32'
39
40
  when 'TEXT', 'STRING'
40
41
  'Edm.String'
41
42
  else
42
- 'Edm.String' if DB_TYPE_STRING_RGX =~ db_type
43
+ 'Edm.String' if DB_TYPE_STRING_RGX =~ db_type.upcase
43
44
  end
44
45
  end
45
46
  end
@@ -1,7 +1,7 @@
1
1
  #!/usr/bin/env ruby
2
2
 
3
3
  require 'rack'
4
- require_relative 'odata/walker.rb'
4
+ require_relative '../odata/walker.rb'
5
5
  require_relative 'request.rb'
6
6
  require_relative 'response.rb'
7
7
 
@@ -1,6 +1,5 @@
1
- #!/usr/bin/env ruby
2
-
3
1
  require 'rack'
2
+ require 'rfc2047'
4
3
 
5
4
  module OData
6
5
  # monkey patch deactivate Rack/multipart because it does not work on simple
@@ -135,8 +134,10 @@ module OData
135
134
 
136
135
  def with_media_data
137
136
  if (filename = @env['HTTP_SLUG'])
138
-
139
- yield @env['rack.input'], content_type.split(';').first, filename
137
+
138
+ yield @env['rack.input'],
139
+ content_type.split(';').first,
140
+ Rfc2047.decode(filename)
140
141
 
141
142
  else
142
143
  ON_CGST_ERROR.call(self)
@@ -1,4 +1,3 @@
1
- #!/usr/bin/env ruby
2
1
  require 'rack'
3
2
 
4
3
  # monkey patch deactivate Rack/multipart because it does not work on simple
@@ -1,5 +1,5 @@
1
1
  #!/usr/bin/env ruby
2
2
 
3
- require_relative './sequel/plugins/join_by_paths.rb'
3
+ require_relative '../sequel/plugins/join_by_paths.rb'
4
4
 
5
5
  Sequel::Model.plugin Sequel::Plugins::JoinByPaths
@@ -46,20 +46,40 @@ module OData
46
46
  end
47
47
  end
48
48
 
49
+ # for expand 1..n nav attributes
50
+ # actually same as v1 get_coll_odata_h
51
+ # def get_expandcoll_odata_h(array:, expand: nil, uribase:, icount: nil)
52
+ # array.map do |w|
53
+ # get_entity_odata_h(entity: w,
54
+ # expand: expand,
55
+ # uribase: uribase)
56
+ # end
57
+ # end
58
+
49
59
  # handle a single expand
50
60
  def handle_entity_expand_one(entity:, exp_one:, nav_values_h:, nav_coll_h:,
51
61
  uribase:)
52
62
 
63
+
53
64
  split_entity_expand_arg(exp_one) do |first, rest_exp|
54
- if (enval = entity.nav_values[first])
55
- nav_values_h[first.to_s] = get_entity_odata_h(entity: enval,
65
+ if ( entity.nav_values.has_key?(first) )
66
+ if (enval = entity.nav_values[first])
67
+ nav_values_h[first.to_s] = get_entity_odata_h(entity: enval,
56
68
  expand: rest_exp,
57
69
  uribase: uribase)
70
+ else
71
+ # FK is NULL --> nav_value is nil --> return empty json
72
+ nav_values_h[first.to_s] = {}
73
+ end
58
74
  elsif (encoll = entity.nav_coll[first])
59
- # nav attributes that are a collection (x..n)
60
- nav_coll_h[first.to_s] = get_coll_odata_h(array: encoll,
75
+ # nav attributes that are a collection (x..n)
76
+ nav_coll_h[first.to_s] = get_coll_odata_h(array: encoll,
61
77
  expand: rest_exp,
62
78
  uribase: uribase)
79
+ # nav_coll_h[first.to_s] = get_expandcoll_odata_h(array: encoll,
80
+ # expand: rest_exp,
81
+ # uribase: uribase)
82
+
63
83
 
64
84
  end
65
85
  end
@@ -71,7 +91,8 @@ module OData
71
91
  explist = expand.split(',')
72
92
  # handle multiple expands
73
93
  explist.each do |exp|
74
- handle_entity_expand_one(entity: entity, exp_one: exp,
94
+ handle_entity_expand_one(entity: entity,
95
+ exp_one: exp,
75
96
  nav_values_h: nav_values_h,
76
97
  nav_coll_h: nav_coll_h,
77
98
  uribase: uribase)
@@ -335,6 +356,7 @@ module OData
335
356
  # to be called at end of publishing block to ensure we get the right names
336
357
  # and additionally build the list of valid attribute path's used
337
358
  # for validation of $orderby or $filter params
359
+
338
360
  def finalize_publishing
339
361
  # build the cmap
340
362
  @cmap = {}
@@ -348,8 +370,10 @@ module OData
348
370
  # set default path prefix if path_prefix was not called
349
371
  path_prefix(DEFAULT_PATH_PREFIX) unless @xpath_prefix
350
372
 
351
- # and finally build the path list
352
- @collections.each(&:build_attribute_path_list)
373
+ @collections.each(&:finalize_publishing)
374
+
375
+ #finalize the media handlers
376
+ @collections.each{|klass| }
353
377
  end
354
378
 
355
379
  def execute_deferred_iblocks
@@ -397,7 +421,7 @@ module OData
397
421
  def add_metadata_xml_entity_type(schema)
398
422
  @collections.each do |klass|
399
423
  enty = klass.add_metadata_rexml(schema)
400
- klass.add_metadata_navs_rexml(enty, @cmap, @relman, @xnamespace)
424
+ klass.add_metadata_navs_rexml(enty, @relman, @xnamespace)
401
425
  end
402
426
  end
403
427
 
@@ -0,0 +1,3 @@
1
+ module Safrano
2
+ VERSION = '0.4.0'
3
+ 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.3.4
4
+ version: 0.4.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - D.M.
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2020-02-22 00:00:00.000000000 Z
11
+ date: 2020-04-26 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: rack
@@ -52,6 +52,20 @@ dependencies:
52
52
  - - "~>"
53
53
  - !ruby/object:Gem::Version
54
54
  version: '5.15'
55
+ - !ruby/object:Gem::Dependency
56
+ name: rfc2047
57
+ requirement: !ruby/object:Gem::Requirement
58
+ requirements:
59
+ - - ">="
60
+ - !ruby/object:Gem::Version
61
+ version: '0'
62
+ type: :runtime
63
+ prerelease: false
64
+ version_requirements: !ruby/object:Gem::Requirement
65
+ requirements:
66
+ - - ">="
67
+ - !ruby/object:Gem::Version
68
+ version: '0'
55
69
  - !ruby/object:Gem::Dependency
56
70
  name: rake
57
71
  requirement: !ruby/object:Gem::Requirement
@@ -95,12 +109,11 @@ dependencies:
95
109
  - !ruby/object:Gem::Version
96
110
  version: '0.51'
97
111
  description: Safrano is an OData server framework based on Ruby, Rack and Sequel.
98
- email: 'dev@aithscel.eu '
112
+ email: dev@aithscel.eu
99
113
  executables: []
100
114
  extensions: []
101
115
  extra_rdoc_files: []
102
116
  files:
103
- - lib/multipart.rb
104
117
  - lib/odata/attribute.rb
105
118
  - lib/odata/batch.rb
106
119
  - lib/odata/collection.rb
@@ -119,16 +132,17 @@ files:
119
132
  - lib/odata/relations.rb
120
133
  - lib/odata/url_parameters.rb
121
134
  - lib/odata/walker.rb
122
- - lib/odata_rack_builder.rb
123
- - lib/rack_app.rb
124
- - lib/request.rb
125
- - lib/response.rb
126
135
  - lib/safrano.rb
127
- - lib/safrano_core.rb
136
+ - lib/safrano/core.rb
137
+ - lib/safrano/multipart.rb
138
+ - lib/safrano/odata_rack_builder.rb
139
+ - lib/safrano/rack_app.rb
140
+ - lib/safrano/request.rb
141
+ - lib/safrano/response.rb
142
+ - lib/safrano/sequel_join_by_paths.rb
143
+ - lib/safrano/service.rb
144
+ - lib/safrano/version.rb
128
145
  - lib/sequel/plugins/join_by_paths.rb
129
- - lib/sequel_join_by_paths.rb
130
- - lib/service.rb
131
- - lib/version.rb
132
146
  homepage: https://gitlab.com/dm0da/safrano
133
147
  licenses:
134
148
  - MIT
@@ -1,4 +0,0 @@
1
-
2
- module Safrano
3
- VERSION = '0.3.4'
4
- end