safrano 0.3.3 → 0.4.3

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.
Files changed (36) hide show
  1. checksums.yaml +4 -4
  2. data/lib/odata/attribute.rb +9 -8
  3. data/lib/odata/batch.rb +8 -8
  4. data/lib/odata/collection.rb +239 -92
  5. data/lib/odata/collection_filter.rb +40 -9
  6. data/lib/odata/collection_media.rb +159 -28
  7. data/lib/odata/collection_order.rb +46 -36
  8. data/lib/odata/common_logger.rb +37 -12
  9. data/lib/odata/entity.rb +188 -99
  10. data/lib/odata/error.rb +60 -12
  11. data/lib/odata/expand.rb +123 -0
  12. data/lib/odata/filter/base.rb +66 -0
  13. data/lib/odata/filter/error.rb +33 -0
  14. data/lib/odata/filter/parse.rb +6 -12
  15. data/lib/odata/filter/sequel.rb +42 -29
  16. data/lib/odata/filter/sequel_function_adapter.rb +147 -0
  17. data/lib/odata/filter/token.rb +5 -1
  18. data/lib/odata/filter/tree.rb +45 -29
  19. data/lib/odata/navigation_attribute.rb +60 -27
  20. data/lib/odata/relations.rb +2 -2
  21. data/lib/odata/select.rb +42 -0
  22. data/lib/odata/url_parameters.rb +51 -36
  23. data/lib/odata/walker.rb +6 -6
  24. data/lib/safrano.rb +23 -13
  25. data/lib/{safrano_core.rb → safrano/core.rb} +12 -4
  26. data/lib/{multipart.rb → safrano/multipart.rb} +17 -26
  27. data/lib/{odata_rack_builder.rb → safrano/odata_rack_builder.rb} +0 -1
  28. data/lib/{rack_app.rb → safrano/rack_app.rb} +12 -10
  29. data/lib/{request.rb → safrano/request.rb} +8 -14
  30. data/lib/{response.rb → safrano/response.rb} +1 -2
  31. data/lib/{sequel_join_by_paths.rb → safrano/sequel_join_by_paths.rb} +1 -1
  32. data/lib/{service.rb → safrano/service.rb} +162 -131
  33. data/lib/safrano/version.rb +3 -0
  34. data/lib/sequel/plugins/join_by_paths.rb +11 -10
  35. metadata +33 -16
  36. data/lib/version.rb +0 -4
@@ -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
@@ -109,13 +108,6 @@ module OData
109
108
  end
110
109
  end
111
110
 
112
- def uribase
113
- return @uribase if @uribase
114
-
115
- @uribase =
116
- "#{env['rack.url_scheme']}://#{env['HTTP_HOST']}#{service.xpath_prefix}"
117
- end
118
-
119
111
  def accept?(type)
120
112
  preferred_type(type).to_s.include?(type)
121
113
  end
@@ -136,11 +128,13 @@ module OData
136
128
  def with_media_data
137
129
  if (filename = @env['HTTP_SLUG'])
138
130
 
139
- yield @env['rack.input'], content_type.split(';').first, filename
131
+ yield @env['rack.input'],
132
+ content_type.split(';').first,
133
+ Rfc2047.decode(filename)
140
134
 
141
135
  else
142
136
  ON_CGST_ERROR.call(self)
143
- return [400, {}, ['File upload error: Missing SLUG']]
137
+ [400, EMPTY_HASH, ['File upload error: Missing SLUG']]
144
138
  end
145
139
  end
146
140
 
@@ -151,15 +145,15 @@ module OData
151
145
  data = JSON.parse(body.read)
152
146
  rescue JSON::ParserError => e
153
147
  ON_CGST_ERROR.call(self)
154
- return [400, {}, ['JSON Parser Error while parsing payload : ',
155
- e.message]]
148
+ return [400, EMPTY_HASH, ['JSON Parser Error while parsing payload : ',
149
+ e.message]]
156
150
  end
157
151
 
158
152
  yield data
159
153
 
160
154
  else # TODO: other formats
161
155
 
162
- [415, {}, []]
156
+ [415, EMPTY_HASH, EMPTY_ARRAY]
163
157
  end
164
158
  end
165
159
 
@@ -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
@@ -35,7 +34,7 @@ module OData
35
34
 
36
35
  if drop_body?
37
36
  close
38
- result = []
37
+ result = EMPTY_ARRAY
39
38
  end
40
39
 
41
40
  if calculate_content_length?
@@ -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
@@ -1,17 +1,19 @@
1
- #!/usr/bin/env ruby
2
-
3
1
  require 'rexml/document'
4
2
  require 'odata/relations.rb'
5
3
  require 'odata/batch.rb'
6
4
  require 'odata/error.rb'
5
+ require 'odata/filter/sequel.rb'
7
6
 
8
7
  module OData
9
8
  # this module has all methods related to expand/defered output preparation
10
9
  # and will be included in Service class
11
10
  module ExpandHandler
12
11
  PATH_SPLITTER = %r{\A(\w+)\/?(.*)\z}.freeze
13
- def get_deferred_odata_h(entity:, attrib:, uribase:)
14
- { '__deferred' => { 'uri' => "#{entity.uri(uribase)}/#{attrib}" } }
12
+ DEFERRED = '__deferred'.freeze
13
+ URI = 'uri'.freeze
14
+
15
+ def get_deferred_odata_h(entity_uri:, attrib:)
16
+ { DEFERRED => { URI => "#{entity_uri}/#{attrib}" } }
15
17
  end
16
18
 
17
19
  # split expand argument into first and rest part
@@ -23,109 +25,75 @@ module OData
23
25
  # TODO: check errorhandling
24
26
  raise OData::ServerError if cur_exp.nil?
25
27
 
26
- k = cur_exp.to_sym
28
+ k_s = cur_exp
29
+
27
30
  else
28
- k = exp_one.strip.to_sym
31
+ k_s = exp_one.strip
29
32
  rest_exp = nil
30
33
  end
31
- yield k, rest_exp
34
+ k = k_s.to_sym
35
+ yield k, k_s, rest_exp
32
36
  end
33
37
 
34
38
  # default v2
35
39
  # overriden in ServiceV1
36
- def get_coll_odata_h(array:, expand: nil, uribase:, icount: nil)
37
- res = array.map do |w|
38
- get_entity_odata_h(entity: w,
39
- expand: expand,
40
- uribase: uribase)
40
+ RESULTS_K = 'results'.freeze
41
+ COUNT_K = '__count'.freeze
42
+ def get_coll_odata_h(array:, template:, icount: nil)
43
+ array.map! do |w|
44
+ get_entity_odata_h(entity: w, template: template)
41
45
  end
42
46
  if icount
43
- { 'results' => res, '__count' => icount }
47
+ { RESULTS_K => array, COUNT_K => icount }
44
48
  else
45
- { 'results' => res }
46
- end
47
- end
48
-
49
- # handle a single expand
50
- def handle_entity_expand_one(entity:, exp_one:, nav_values_h:, nav_coll_h:,
51
- uribase:)
52
-
53
- 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,
56
- expand: rest_exp,
57
- uribase: uribase)
58
- 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,
61
- expand: rest_exp,
62
- uribase: uribase)
63
-
64
- end
65
- end
66
- end
67
-
68
- def handle_entity_expand(entity:, expand:, nav_values_h:,
69
- nav_coll_h:, uribase:)
70
- expand.strip!
71
- explist = expand.split(',')
72
- # handle multiple expands
73
- explist.each do |exp|
74
- handle_entity_expand_one(entity: entity, exp_one: exp,
75
- nav_values_h: nav_values_h,
76
- nav_coll_h: nav_coll_h,
77
- uribase: uribase)
78
- end
79
- end
80
-
81
- def handle_entity_deferred_attribs(entity:, nav_values_h:,
82
- nav_coll_h:, uribase:)
83
- entity.nav_values.each_key do |ksy|
84
- ks = ksy.to_s
85
- next if nav_values_h.key?(ks)
86
-
87
- nav_values_h[ks] = get_deferred_odata_h(entity: entity,
88
- attrib: ks, uribase: uribase)
89
- end
90
- entity.nav_coll.each_key do |ksy|
91
- ks = ksy.to_s
92
- next if nav_coll_h.key?(ks)
93
-
94
- nav_coll_h[ks] = get_deferred_odata_h(entity: entity, attrib: ks,
95
- uribase: uribase)
49
+ { RESULTS_K => array }
96
50
  end
97
51
  end
98
52
 
99
53
  # handle $links ... Note: $expand seems to be ignored when $links
100
54
  # are requested
101
- def get_entity_odata_link_h(entity:, uribase:)
102
- { uri: entity.uri(uribase) }
103
- end
104
-
105
- def get_entity_odata_h(entity:, expand: nil, uribase:)
106
- hres = entity.casted_values
107
- hres['__metadata'] = entity.metadata_h(uribase: uribase)
108
-
109
- nav_values_h = {}
110
- nav_coll_h = {}
55
+ def get_entity_odata_link_h(entity:)
56
+ { uri: entity.uri }
57
+ end
58
+
59
+ EMPTYH = {}.freeze
60
+ METADATA_K = '__metadata'.freeze
61
+ def get_entity_odata_h(entity:, template:)
62
+ # start with metadata
63
+ hres = { METADATA_K => entity.metadata_h }
64
+
65
+ template.each do |elmt, arg|
66
+ case elmt
67
+ when :all_values
68
+ hres.merge! entity.casted_values
69
+ when :selected_vals
70
+ hres.merge! entity.casted_values(arg)
71
+ when :expand_e
72
+
73
+ arg.each do |attr, templ|
74
+ enval = entity.send(attr)
75
+ hres[attr] = if enval
76
+ get_entity_odata_h(entity: enval, template: templ)
77
+ else
78
+ # FK is NULL --> nav_value is nil --> return empty json
79
+ EMPTYH
80
+ end
81
+ end
82
+ when :expand_c
83
+ arg.each do |attr, templ|
84
+ next unless (encoll = entity.send(attr))
111
85
 
112
- # handle expanded nav attributes
113
- unless expand.nil?
114
- handle_entity_expand(entity: entity, expand: expand,
115
- nav_values_h: nav_values_h,
116
- nav_coll_h: nav_coll_h,
117
- uribase: uribase)
86
+ # nav attributes that are a collection (x..n)
87
+ hres[attr] = get_coll_odata_h(array: encoll, template: templ)
88
+ # else error ?
89
+ end
90
+ when :deferr
91
+ euri = entity.uri
92
+ arg.each do |attr|
93
+ hres[attr] = get_deferred_odata_h(entity_uri: euri, attrib: attr)
94
+ end
95
+ end
118
96
  end
119
-
120
- # handle not expanded (deferred) nav attributes
121
- handle_entity_deferred_attribs(entity: entity,
122
- nav_values_h: nav_values_h,
123
- nav_coll_h: nav_coll_h,
124
- uribase: uribase)
125
- # merge ...
126
- hres.merge!(nav_values_h)
127
- hres.merge!(nav_coll_h)
128
-
129
97
  hres
130
98
  end
131
99
  end
@@ -175,7 +143,8 @@ module OData
175
143
  attr_accessor :xname
176
144
  attr_accessor :xnamespace
177
145
  attr_accessor :xpath_prefix
178
- # attr_accessor :xuribase
146
+ attr_accessor :xserver_url
147
+ attr_accessor :uribase
179
148
  attr_accessor :meta
180
149
  attr_accessor :batch_handler
181
150
  attr_accessor :relman
@@ -200,7 +169,8 @@ module OData
200
169
 
201
170
  DATASERVICEVERSION_RGX = /\A([1234])(?:\.0);*\w*\z/.freeze
202
171
  TRAILING_SLASH = %r{/\z}.freeze
203
- DEFAULT_PATH_PREFIX = '/'
172
+ DEFAULT_PATH_PREFIX = '/'.freeze
173
+ DEFAULT_SERVER_URL = 'http://localhost:9494'
204
174
 
205
175
  # input is the DataServiceVersion request header string, eg.
206
176
  # '2.0;blabla' ---> Version -> 2
@@ -243,40 +213,67 @@ module OData
243
213
  (@v1.xpath_prefix = @xpath_prefix) if @v1
244
214
  (@v2.xpath_prefix = @xpath_prefix) if @v2
245
215
  end
216
+
217
+ def server_url(surl)
218
+ @xserver_url = surl.sub(TRAILING_SLASH, '')
219
+ (@v1.xserver_url = @xserver_url) if @v1
220
+ (@v2.xserver_url = @xserver_url) if @v2
221
+ end
222
+
246
223
  # end public API
247
224
 
225
+ def set_uribase
226
+ @uribase = if @xpath_prefix.empty?
227
+ @xserver_url
228
+ else
229
+ if @xpath_prefix[0] == '/'
230
+ @xserver_url + @xpath_prefix
231
+ else
232
+ @xserver_url + '/' + @xpath_prefix
233
+ end
234
+ end
235
+ (@v1.uribase = @uribase) if @v1
236
+ (@v2.uribase = @uribase) if @v2
237
+ end
238
+
248
239
  def copy_attribs_to(other)
249
240
  other.cmap = @cmap
250
241
  other.collections = @collections
242
+ other.allowed_transitions = @allowed_transitions
251
243
  other.xtitle = @xtitle
252
244
  other.xname = @xname
253
245
  other.xnamespace = @xnamespace
254
246
  other.xpath_prefix = @xpath_prefix
247
+ other.xserver_url = @xserver_url
248
+ other.uribase = @uribase
255
249
  other.meta = ServiceMeta.new(other) # hum ... #todo: versions as well ?
256
250
  other.relman = @relman
257
251
  other.batch_handler = @batch_handler
258
252
  other
259
253
  end
260
254
 
255
+ # this is a central place. We extend Sequel models with OData functionality
256
+ # The included/extended modules depends on the properties(eg, pks, field types) of the model
257
+ # we differentiate
258
+ # * Single/Multi PK
259
+ # * Media/Non-Media entity
260
+ # Putting this logic here in modules loaded once on start shall result in less runtime overhead
261
261
  def register_model(modelklass, entity_set_name = nil, is_media = false)
262
262
  # check that the provided klass is a Sequel Model
263
- unless modelklass.is_a? Sequel::Model::ClassMethods
264
- raise OData::API::ModelNameError, modelklass
265
- end
263
+
264
+ raise(OData::API::ModelNameError, modelklass) unless modelklass.is_a? Sequel::Model::ClassMethods
266
265
 
267
266
  if modelklass.ancestors.include? OData::Entity
268
267
  # modules were already added previously;
269
268
  # cleanup state to avoid having data from previous calls
270
269
  # mostly usefull for testing (eg API)
271
270
  modelklass.reset
272
- else # first API call... (normal non-testing case)
273
- if modelklass.primary_key.is_a?(Array)
274
- modelklass.extend OData::EntityClassMultiPK
275
- modelklass.include OData::EntityMultiPK
276
- else
277
- modelklass.extend OData::EntityClassSinglePK
278
- modelklass.include OData::EntitySinglePK
279
- end
271
+ elsif modelklass.primary_key.is_a?(Array) # first API call... (normal non-testing case)
272
+ modelklass.extend OData::EntityClassMultiPK
273
+ modelklass.include OData::EntityMultiPK
274
+ else
275
+ modelklass.extend OData::EntityClassSinglePK
276
+ modelklass.include OData::EntitySinglePK
280
277
  end
281
278
  # Media/Non-media
282
279
  if is_media
@@ -326,15 +323,14 @@ module OData
326
323
  # example: CrewMember must be matched before Crew otherwise we get error
327
324
  def set_collections_sorted(coll_data)
328
325
  @collections = coll_data
329
- if @collections
330
- @collections.sort_by! { |klass| klass.entity_set_name.size }.reverse!
331
- end
326
+ @collections.sort_by! { |klass| klass.entity_set_name.size }.reverse! if @collections
332
327
  @collections
333
328
  end
334
329
 
335
330
  # to be called at end of publishing block to ensure we get the right names
336
331
  # and additionally build the list of valid attribute path's used
337
332
  # for validation of $orderby or $filter params
333
+
338
334
  def finalize_publishing
339
335
  # build the cmap
340
336
  @cmap = {}
@@ -348,8 +344,36 @@ module OData
348
344
  # set default path prefix if path_prefix was not called
349
345
  path_prefix(DEFAULT_PATH_PREFIX) unless @xpath_prefix
350
346
 
351
- # and finally build the path list
352
- @collections.each(&:build_attribute_path_list)
347
+ # set default server url if server_url was not called
348
+ server_url(DEFAULT_SERVER_URL) unless @xserver_url
349
+
350
+ set_uribase
351
+
352
+ @collections.each(&:finalize_publishing)
353
+
354
+ # finalize the uri's and include NoMappingBeforeOutput or MappingBeforeOutput as needed
355
+ @collections.each { |klass|
356
+ klass.build_uri(@uribase)
357
+ if klass.time_cols.empty?
358
+ klass.include OData::NoMappingBeforeOutput
359
+ else
360
+ klass.include OData::MappingBeforeOutput
361
+ end
362
+ }
363
+
364
+ # build allowed transitions (requires that @collections are filled and sorted for having a
365
+ # correct base_url_regexp)
366
+ build_allowed_transitions
367
+
368
+ # mixin adapter specific modules where needed
369
+ case Sequel::Model.db.adapter_scheme
370
+ when :postgres
371
+ OData::Filter::FuncTree.include OData::Filter::FuncTreePostgres
372
+ when :sqlite
373
+ OData::Filter::FuncTree.include OData::Filter::FuncTreeSqlite
374
+ else
375
+ OData::Filter::FuncTree.include OData::Filter::FuncTreeDefault
376
+ end
353
377
  end
354
378
 
355
379
  def execute_deferred_iblocks
@@ -374,7 +398,7 @@ module OData
374
398
  def service_xml(req)
375
399
  doc = REXML::Document.new
376
400
  # separator required ? ?
377
- root = doc.add_element('service', 'xml:base' => req.uribase)
401
+ root = doc.add_element('service', 'xml:base' => @uribase)
378
402
 
379
403
  root.add_namespace('xmlns:atom', XMLNS::W3_2005_ATOM)
380
404
  root.add_namespace('xmlns:app', XMLNS::W3_2007_APP)
@@ -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
 
@@ -485,15 +509,22 @@ module OData
485
509
  # methods related to transitions to next state (cf. walker)
486
510
  module Transitions
487
511
  DOLLAR_ID_REGEXP = Regexp.new('\A\/\$').freeze
488
- def allowed_transitions
489
- @allowed_transitions = [
490
- Safrano::TransitionEnd,
491
- Safrano::TransitionMetadata,
492
- Safrano::TransitionBatch,
493
- Safrano::TransitionContentId,
512
+ ALLOWED_TRANSITIONS_FIXED = [
513
+ Safrano::TransitionEnd,
514
+ Safrano::TransitionMetadata,
515
+ Safrano::TransitionBatch,
516
+ Safrano::TransitionContentId
517
+ ].freeze
518
+
519
+ def build_allowed_transitions
520
+ @allowed_transitions = (ALLOWED_TRANSITIONS_FIXED + [
494
521
  Safrano::Transition.new(%r{\A/(#{base_url_regexp})(.*)},
495
522
  trans: 'transition_collection')
496
- ]
523
+ ]).freeze
524
+ end
525
+
526
+ def allowed_transitions
527
+ @allowed_transitions
497
528
  end
498
529
 
499
530
  def transition_collection(match_result)
@@ -540,22 +571,22 @@ module OData
540
571
  @data_service_version = '1.0'
541
572
  end
542
573
 
543
- def get_coll_odata_links_h(array:, uribase:, icount: nil)
574
+ def get_coll_odata_links_h(array:, icount: nil)
544
575
  array.map do |w|
545
- get_entity_odata_link_h(entity: w, uribase: uribase)
576
+ get_entity_odata_link_h(entity: w)
546
577
  end
547
578
  end
548
579
 
549
- def get_coll_odata_h(array:, expand: nil, uribase:, icount: nil)
550
- array.map do |w|
580
+ def get_coll_odata_h(array:, template:, icount: nil)
581
+ array.map! do |w|
551
582
  get_entity_odata_h(entity: w,
552
- expand: expand,
553
- uribase: uribase)
583
+ template: template)
554
584
  end
585
+ array
555
586
  end
556
587
 
557
588
  def get_emptycoll_odata_h
558
- [{}]
589
+ EMPTY_HASH_IN_ARY
559
590
  end
560
591
  end
561
592
 
@@ -565,19 +596,19 @@ module OData
565
596
  @data_service_version = '2.0'
566
597
  end
567
598
 
568
- def get_coll_odata_links_h(array:, uribase:, icount: nil)
599
+ def get_coll_odata_links_h(array:, icount: nil)
569
600
  res = array.map do |w|
570
- get_entity_odata_link_h(entity: w, uribase: uribase)
601
+ get_entity_odata_link_h(entity: w)
571
602
  end
572
603
  if icount
573
- { 'results' => res, '__count' => icount }
604
+ { RESULTS_K => res, COUNT_K => icount }
574
605
  else
575
- { 'results' => res }
606
+ { RESULTS_K => res }
576
607
  end
577
608
  end
578
609
 
579
610
  def get_emptycoll_odata_h
580
- { 'results' => [{}] }
611
+ { RESULTS_K => EMPTY_HASH_IN_ARY }
581
612
  end
582
613
  end
583
614
 
@@ -588,9 +619,9 @@ module OData
588
619
  @service = service
589
620
  end
590
621
 
622
+ ALLOWED_TRANSITIONS_FIXED = [Safrano::TransitionEnd].freeze
591
623
  def allowed_transitions
592
- @allowed_transitions = [Safrano::Transition.new('',
593
- trans: 'transition_end')]
624
+ ALLOWED_TRANSITIONS_FIXED
594
625
  end
595
626
 
596
627
  def transition_end(_match_result)