safrano 0.4.3 → 0.4.4

Sign up to get free protection for your applications and to get access to all the features.
Files changed (54) hide show
  1. checksums.yaml +4 -4
  2. data/lib/core_ext/Dir/iter.rb +18 -0
  3. data/lib/core_ext/Hash/transform.rb +21 -0
  4. data/lib/core_ext/Integer/edm.rb +13 -0
  5. data/lib/core_ext/REXML/Document/output.rb +16 -0
  6. data/lib/core_ext/String/convert.rb +25 -0
  7. data/lib/core_ext/String/edm.rb +13 -0
  8. data/lib/core_ext/dir.rb +3 -0
  9. data/lib/core_ext/hash.rb +3 -0
  10. data/lib/core_ext/integer.rb +3 -0
  11. data/lib/core_ext/rexml.rb +3 -0
  12. data/lib/core_ext/string.rb +5 -0
  13. data/lib/odata/attribute.rb +6 -2
  14. data/lib/odata/batch.rb +9 -7
  15. data/lib/odata/collection.rb +136 -642
  16. data/lib/odata/collection_filter.rb +16 -40
  17. data/lib/odata/collection_media.rb +56 -37
  18. data/lib/odata/collection_order.rb +5 -2
  19. data/lib/odata/common_logger.rb +2 -0
  20. data/lib/odata/complex_type.rb +152 -0
  21. data/lib/odata/edm/primitive_types.rb +184 -0
  22. data/lib/odata/entity.rb +53 -117
  23. data/lib/odata/error.rb +142 -37
  24. data/lib/odata/expand.rb +20 -17
  25. data/lib/odata/filter/base.rb +4 -1
  26. data/lib/odata/filter/error.rb +43 -27
  27. data/lib/odata/filter/parse.rb +33 -25
  28. data/lib/odata/filter/sequel.rb +97 -56
  29. data/lib/odata/filter/sequel_function_adapter.rb +50 -49
  30. data/lib/odata/filter/token.rb +10 -10
  31. data/lib/odata/filter/tree.rb +75 -41
  32. data/lib/odata/function_import.rb +166 -0
  33. data/lib/odata/model_ext.rb +618 -0
  34. data/lib/odata/navigation_attribute.rb +9 -24
  35. data/lib/odata/relations.rb +5 -5
  36. data/lib/odata/select.rb +17 -5
  37. data/lib/odata/transition.rb +71 -0
  38. data/lib/odata/url_parameters.rb +100 -24
  39. data/lib/odata/walker.rb +15 -7
  40. data/lib/safrano.rb +18 -38
  41. data/lib/safrano/contract.rb +143 -0
  42. data/lib/safrano/core.rb +12 -94
  43. data/lib/safrano/core_ext.rb +13 -0
  44. data/lib/safrano/deprecation.rb +73 -0
  45. data/lib/safrano/multipart.rb +25 -20
  46. data/lib/safrano/rack_app.rb +61 -62
  47. data/lib/safrano/{odata_rack_builder.rb → rack_builder.rb} +18 -1
  48. data/lib/safrano/request.rb +95 -37
  49. data/lib/safrano/response.rb +4 -2
  50. data/lib/safrano/sequel_join_by_paths.rb +2 -2
  51. data/lib/safrano/service.rb +132 -94
  52. data/lib/safrano/version.rb +3 -1
  53. data/lib/sequel/plugins/join_by_paths.rb +6 -19
  54. metadata +24 -5
@@ -1,8 +1,10 @@
1
+ # frozen_string_literal: true
2
+
1
3
  require 'json'
2
- require_relative '../safrano/core.rb'
3
- require_relative './entity.rb'
4
+ require_relative '../safrano/core'
5
+ require_relative './entity'
4
6
 
5
- module OData
7
+ module Safrano
6
8
  # remove the relation between entity and parent by clearing
7
9
  # the FK field(s) (if allowed)
8
10
  def self.remove_nav_relation(assoc, parent)
@@ -73,29 +75,15 @@ module OData
73
75
  end
74
76
  end
75
77
 
76
- module EntityBase
77
- module NavigationInfo
78
- attr_reader :nav_parent
79
- attr_reader :navattr_reflection
80
- attr_reader :nav_name
81
- def set_relation_info(parent, name)
82
- @nav_parent = parent
83
- @nav_name = name
84
- @navattr_reflection = parent.class.association_reflections[name.to_sym]
85
- @nav_klass = @navattr_reflection[:class_name].constantize
86
- end
87
- end
88
- end
89
-
90
78
  # Represents a named but nil-valued navigation-attribute of an Entity
91
79
  # (usually resulting from a NULL FK db value)
92
80
  class NilNavigationAttribute
93
- include EntityBase::NavigationInfo
81
+ include Safrano::NavigationInfo
94
82
  def odata_get(req)
95
83
  if req.walker.media_value
96
- OData::ErrorNotFound.odata_get
84
+ Safrano::ErrorNotFound.odata_get
97
85
  elsif req.accept?(APPJSON)
98
- [200, CT_JSON, to_odata_json(service: req.service)]
86
+ [200, EMPTY_HASH, to_odata_json(service: req.service)]
99
87
  else # TODO: other formats
100
88
  415
101
89
  end
@@ -111,13 +99,10 @@ module OData
111
99
 
112
100
  # create the nav. entity
113
101
  def odata_put(req)
114
- # if req.walker.raw_value
115
102
  # delegate to the class method
116
103
  @nav_klass.odata_create_entity_and_relation(req,
117
104
  @navattr_reflection,
118
105
  @nav_parent)
119
- # else
120
- # end
121
106
  end
122
107
 
123
108
  # empty output as OData json (v2)
@@ -133,7 +118,7 @@ module OData
133
118
  # methods related to transitions to next state (cf. walker)
134
119
  module Transitions
135
120
  def transition_end(_match_result)
136
- [nil, :end]
121
+ Safrano::Transition::RESULT_END
137
122
  end
138
123
 
139
124
  def transition_value(_match_result)
@@ -1,9 +1,9 @@
1
- #!/usr/bin/env ruby
1
+ # frozen_string_literal: true
2
2
 
3
3
  require 'set'
4
4
 
5
5
  # OData relation related classes/module
6
- module OData
6
+ module Safrano
7
7
  # we represent a relation as a Set (unordered) of two end elements
8
8
  class Relation < Set
9
9
  # attr_reader :rid
@@ -20,7 +20,7 @@ module OData
20
20
 
21
21
  # we need a from/to order independant ID
22
22
  def rid
23
- OData::RelationManager.build_id(self)
23
+ Safrano::RelationManager.build_id(self)
24
24
  end
25
25
 
26
26
  # we need a from/to order independant OData like name
@@ -76,11 +76,11 @@ module OData
76
76
  end
77
77
 
78
78
  def get(arg)
79
- rid = OData::RelationManager.build_id(arg)
79
+ rid = Safrano::RelationManager.build_id(arg)
80
80
  if @list.key?(rid)
81
81
  @list[rid]
82
82
  else
83
- rel = OData::Relation.new(arg)
83
+ rel = Safrano::Relation.new(arg)
84
84
  @list[rid] = rel
85
85
  end
86
86
  end
@@ -1,8 +1,10 @@
1
- require 'odata/error.rb'
1
+ # frozen_string_literal: true
2
+
3
+ require 'odata/error'
2
4
 
3
5
  # all dataset selecting related classes in our OData module
4
6
  # ie do eager loading
5
- module OData
7
+ module Safrano
6
8
  # base class for selecting. We have to distinguish between
7
9
  # fields of the current entity, and the navigation properties
8
10
  # we can have one special case
@@ -12,12 +14,12 @@ module OData
12
14
  class SelectBase
13
15
  ALL = new # re-useable selecting-all handler
14
16
 
15
- def self.factory(selectstr)
17
+ def self.factory(selectstr, model)
16
18
  case selectstr&.strip
17
19
  when nil, '', '*'
18
20
  ALL
19
21
  else
20
- Select.new(selectstr)
22
+ Select.new(selectstr, model)
21
23
  end
22
24
  end
23
25
 
@@ -28,15 +30,25 @@ module OData
28
30
  def ALL.all_props?
29
31
  true
30
32
  end
33
+
34
+ def parse_error?
35
+ Contract::OK
36
+ end
31
37
  end
32
38
 
33
39
  # single select
34
40
  class Select < SelectBase
35
41
  COMASPLIT = /\s*,\s*/.freeze
36
42
  attr_reader :props
37
- def initialize(selstr)
43
+
44
+ def initialize(selstr, model)
45
+ @model = model
38
46
  @selectp = selstr.strip
39
47
  @props = @selectp.split(COMASPLIT)
40
48
  end
49
+
50
+ def parse_error?
51
+ (invalids = @model.find_invalid_props(props.to_set)) ? BadRequestSelectInvalidProps.new(@model, invalids) : Contract::OK
52
+ end
41
53
  end
42
54
  end
@@ -0,0 +1,71 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative 'error'
4
+
5
+ # our main namespace
6
+ module Safrano
7
+ # represents a state transition when navigating/parsing the url path
8
+ # from left to right
9
+ class Transition < Regexp
10
+ attr_accessor :trans
11
+ attr_accessor :match_result
12
+ attr_accessor :rgx
13
+ attr_reader :remain_idx
14
+
15
+ EMPTYSTR = ''.freeze
16
+ SLASH = '/'.freeze
17
+
18
+ RESULT_BAD_REQ_ERR = [nil, :error, ::Safrano::BadRequestError].freeze
19
+ RESULT_NOT_FOUND_ERR = [nil, :error, ::Safrano::ErrorNotFound].freeze
20
+ RESULT_SERVER_TR_ERR = [nil, :error, ServerTransitionError].freeze
21
+ RESULT_END = [nil, :end].freeze
22
+
23
+ def initialize(arg, trans: nil, remain_idx: 2)
24
+ @rgx = if arg.respond_to? :each_char
25
+ Regexp.new(arg)
26
+ else
27
+ arg
28
+ end
29
+ @trans = trans
30
+ @remain_idx = remain_idx
31
+ end
32
+
33
+ def do_match(str)
34
+ @match_result = @rgx.match(str)
35
+ end
36
+
37
+ # remain_idx is the index of the last match-data. ususally its 2
38
+ # but can be overidden
39
+ def path_remain
40
+ @match_result[@remain_idx] if @match_result && @match_result[@remain_idx]
41
+ end
42
+
43
+ def path_done
44
+ if @match_result
45
+ @match_result[1] || EMPTYSTR
46
+ else
47
+ EMPTYSTR
48
+ end
49
+ end
50
+
51
+ def do_transition(ctx)
52
+ ctx.method(@trans).call(@match_result)
53
+ end
54
+ end
55
+
56
+ TransitionEnd = Transition.new('\A(\/?)\z', trans: 'transition_end')
57
+ TransitionMetadata = Transition.new('\A(\/\$metadata)(.*)',
58
+ trans: 'transition_metadata')
59
+ TransitionBatch = Transition.new('\A(\/\$batch)(.*)',
60
+ trans: 'transition_batch')
61
+ TransitionContentId = Transition.new('\A(\/\$(\d+))(.*)',
62
+ trans: 'transition_content_id',
63
+ remain_idx: 3)
64
+ TransitionCount = Transition.new('(\A\/\$count)(.*)\z',
65
+ trans: 'transition_count')
66
+ TransitionValue = Transition.new('(\A\/\$value)(.*)\z',
67
+ trans: 'transition_value')
68
+ TransitionLinks = Transition.new('(\A\/\$links)(.*)\z',
69
+ trans: 'transition_links')
70
+ attr_accessor :allowed_transitions
71
+ end
@@ -1,23 +1,50 @@
1
- require 'odata/error.rb'
1
+ # frozen_string_literal: true
2
+
3
+ require 'odata/error'
2
4
 
3
5
  # url parameters processing . Mostly delegates to specialised classes
4
6
  # (filter, order...) to convert into Sequel exprs.
5
- module OData
7
+ module Safrano
6
8
  class UrlParametersBase
7
9
  attr_reader :expand
8
10
  attr_reader :select
9
11
 
12
+ # url params validation methods.
13
+ # nil is the expected return for no errors
14
+ # an error class is returned in case of errors
15
+ # this way we can combine multiple validation calls with logical ||
10
16
  def check_expand
11
- return BadRequestExpandParseError if @expand.parse_error?
17
+ @expand.parse_error?
18
+ end
19
+
20
+ def check_select
21
+ @select.parse_error?
22
+ end
23
+
24
+ def initialize(dataset, params = {})
25
+ @model = if dataset.respond_to? :model
26
+ dataset.model
27
+ else
28
+ dataset
29
+ end
30
+ @params = params
12
31
  end
13
32
  end
14
33
 
15
34
  # url parameters for a single entity expand/select
16
35
  class UrlParameters4Single < UrlParametersBase
17
- def initialize(params)
18
- @params = params
19
- @expand = ExpandBase.factory(@params['$expand'])
20
- @select = SelectBase.factory(@params['$select'])
36
+ def initialize(dataset, params)
37
+ super
38
+ @expand = ExpandBase.factory(@params['$expand'], @model)
39
+ @select = SelectBase.factory(@params['$select'], @model)
40
+ end
41
+
42
+ def check_all
43
+ return Contract::OK unless @params
44
+
45
+ check_expand.if_valid do
46
+ check_select
47
+ end
21
48
  end
22
49
  end
23
50
 
@@ -26,49 +53,98 @@ module OData
26
53
  attr_reader :filt
27
54
  attr_reader :ordby
28
55
 
29
- def initialize(model, params)
56
+ def initialize(dataset, params = {})
57
+ super
30
58
  # join helper is only needed for odering or filtering
31
- @jh = model.join_by_paths_helper if params['$orderby'] || params['$filter']
59
+ @jh = @model.join_by_paths_helper if params['$orderby'] || params['$filter']
32
60
  @params = params
33
61
  @ordby = OrderBase.factory(@params['$orderby'], @jh)
34
62
  @filt = FilterBase.factory(@params['$filter'])
35
- @expand = ExpandBase.factory(@params['$expand'])
36
- @select = SelectBase.factory(@params['$select'])
63
+ @expand = ExpandBase.factory(@params['$expand'], @model)
64
+ @select = SelectBase.factory(@params['$select'], @model)
65
+ end
66
+
67
+ def check_top
68
+ return Contract::OK unless @params['$top']
69
+
70
+ itop = number_or_nil(@params['$top'])
71
+ (itop.nil? || itop.zero?) ? BadRequestError : Contract::OK
72
+ end
73
+
74
+ def check_skip
75
+ return Contract::OK unless @params['$skip']
76
+
77
+ iskip = number_or_nil(@params['$skip'])
78
+ (iskip.nil? || iskip.negative?) ? BadRequestError : Contract::OK
79
+ end
80
+
81
+ def check_inlinecount
82
+ return Contract::OK unless (icp = @params['$inlinecount'])
83
+
84
+ ((icp == 'allpages') || (icp == 'none')) ? Contract::OK : BadRequestInlineCountParamError
37
85
  end
38
86
 
39
87
  def check_filter
40
- return BadRequestFilterParseError if @filt.parse_error?
88
+ (err = @filt.parse_error?) ? err : Contract::OK
41
89
  end
42
90
 
43
- def check_order
91
+ def check_orderby
92
+ return Contract::OK if @ordby.empty?
44
93
  return BadRequestOrderParseError if @ordby.parse_error?
94
+
95
+ Contract::OK
45
96
  end
46
97
 
47
98
  def apply_to_dataset(dtcx)
48
- dtcx = apply_expand_to_dataset(dtcx)
49
- apply_filter_order_to_dataset(dtcx)
99
+ apply_expand_to_dataset(dtcx).if_valid do |dataset|
100
+ apply_filter_order_to_dataset(dataset)
101
+ end
50
102
  end
51
103
 
52
104
  def apply_expand_to_dataset(dtcx)
53
- return dtcx if @expand.empty?
105
+ return Contract.valid(dtcx) if @expand.empty?
54
106
 
55
107
  @expand.apply_to_dataset(dtcx)
56
108
  end
57
109
 
58
110
  # Warning, the @ordby and @filt objects are coupled by way of the join helper
59
111
  def apply_filter_order_to_dataset(dtcx)
60
- return dtcx if @filt.empty? && @ordby.empty?
112
+ return Contract.valid(dtcx) if @filt.empty? && @ordby.empty?
61
113
 
62
- # filter object and join-helper need to be finalized after filter has been parsed and checked
63
- @filt.finalize(@jh)
114
+ # filter object and join-helper need to be finalized after filter
115
+ # has been parsed and checked
116
+ @filt.finalize(@jh).if_valid do
117
+ # start with the join
118
+ dtcx = @jh.dataset(dtcx)
64
119
 
65
- # start with the join
66
- dtcx = @jh.dataset(dtcx)
120
+ @filt.apply_to_dataset(dtcx).map_result! do |dataset|
121
+ dtcx = dataset
122
+ dtcx = @ordby.apply_to_dataset(dtcx)
123
+ dtcx.select_all(@jh.start_model.table_name)
124
+ end
125
+ end
126
+ end
67
127
 
68
- dtcx = @filt.apply_to_dataset(dtcx)
69
- dtcx = @ordby.apply_to_dataset(dtcx)
128
+ ###########################################################
129
+ def check_all
130
+ return Contract::OK unless @params
70
131
 
71
- dtcx.select_all(@jh.start_model.table_name)
132
+ # lazy nested proc evaluation.
133
+ # if one check fails, it will be passed up the chain and the ones
134
+ # below will not be evaluated
135
+ check_top.if_valid do
136
+ check_skip.if_valid do
137
+ check_orderby.if_valid do
138
+ check_filter.if_valid do
139
+ check_expand.if_valid do
140
+ check_select.if_valid do
141
+ check_inlinecount
142
+ end
143
+ end
144
+ end
145
+ end
146
+ end
147
+ end
72
148
  end
73
149
  end
74
150
  end
@@ -1,8 +1,10 @@
1
+ # frozen_string_literal: true
2
+
1
3
  require 'json'
2
4
  require 'rexml/document'
3
- require 'safrano.rb'
5
+ require 'safrano'
4
6
 
5
- module OData
7
+ module Safrano
6
8
  # handle navigation in the Datamodel tree of entities/attributes
7
9
  # input is the url path. Url parameters ($filter etc...) are NOT handled here
8
10
  # This uses a state transition algorithm
@@ -27,9 +29,11 @@ module OData
27
29
 
28
30
  # are $links requested ?
29
31
  attr_reader :do_links
32
+
30
33
  NIL_SERVICE_FATAL = 'Walker is called with a nil service'.freeze
31
34
  EMPTYSTR = ''.freeze
32
35
  SLASH = '/'.freeze
36
+
33
37
  def initialize(service, path, content_id_refs = nil)
34
38
  raise NIL_SERVICE_FATAL unless service
35
39
 
@@ -42,10 +46,9 @@ module OData
42
46
  @path_start = @path_remain = if service
43
47
  unprefixed(service.xpath_prefix, path)
44
48
  else # This is for batch function
45
-
46
49
  path
47
50
  end
48
- @path_done = ''
51
+ @path_done = String.new
49
52
  @status = :start
50
53
  @end_context = nil
51
54
  @do_count = nil
@@ -74,6 +77,7 @@ module OData
74
77
  valid_tr = @context.allowed_transitions.select do |t|
75
78
  t.do_match(@path_remain)
76
79
  end
80
+
77
81
  # this is a very fragile and obscure but required hack (wanted: a
78
82
  # better one) to make attributes that are substrings of each other
79
83
  # work well
@@ -95,13 +99,13 @@ module OData
95
99
  @context = nil
96
100
  @status = :error
97
101
  # TODO: more appropriate error handling
98
- @error = OData::ErrorNotFound
102
+ @error = Safrano::ErrorNotFound
99
103
  end
100
104
  else
101
105
  @context = nil
102
106
  @status = :error
103
107
  # TODO: more appropriate error handling
104
- @error = OData::ErrorNotFound
108
+ @error = Safrano::ErrorNotFound
105
109
  end
106
110
  end
107
111
 
@@ -151,7 +155,7 @@ module OData
151
155
  else
152
156
  @context = nil
153
157
  @status = :error
154
- @error = OData::ErrorNotFound
158
+ @error = Safrano::ErrorNotFoundSegment.new(@path_remain)
155
159
  end
156
160
  end
157
161
  # TODO: shouldnt we raise an error here if @status != :end ?
@@ -159,5 +163,9 @@ module OData
159
163
 
160
164
  @end_context = @contexts.size >= 2 ? @contexts[-2] : @contexts[1]
161
165
  end
166
+
167
+ def finalize
168
+ (@status == :end) ? Contract.valid(@end_context) : @error
169
+ end
162
170
  end
163
171
  end