safrano 0.3.4 → 0.4.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
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