safrano 0.4.2 → 0.5.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.
Files changed (54) hide show
  1. checksums.yaml +4 -4
  2. data/lib/core_ext/Dir/iter.rb +18 -0
  3. data/lib/core_ext/Hash/transform.rb +21 -0
  4. data/lib/core_ext/Integer/edm.rb +13 -0
  5. data/lib/core_ext/REXML/Document/output.rb +16 -0
  6. data/lib/core_ext/String/convert.rb +25 -0
  7. data/lib/core_ext/String/edm.rb +13 -0
  8. data/lib/core_ext/dir.rb +3 -0
  9. data/lib/core_ext/hash.rb +3 -0
  10. data/lib/core_ext/integer.rb +3 -0
  11. data/lib/core_ext/rexml.rb +3 -0
  12. data/lib/core_ext/string.rb +5 -0
  13. data/lib/odata/attribute.rb +15 -10
  14. data/lib/odata/batch.rb +9 -7
  15. data/lib/odata/collection.rb +140 -591
  16. data/lib/odata/collection_filter.rb +18 -42
  17. data/lib/odata/collection_media.rb +111 -54
  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 +123 -172
  23. data/lib/odata/error.rb +183 -32
  24. data/lib/odata/expand.rb +20 -17
  25. data/lib/odata/filter/base.rb +74 -0
  26. data/lib/odata/filter/error.rb +49 -6
  27. data/lib/odata/filter/parse.rb +41 -25
  28. data/lib/odata/filter/sequel.rb +133 -62
  29. data/lib/odata/filter/sequel_function_adapter.rb +148 -0
  30. data/lib/odata/filter/token.rb +26 -19
  31. data/lib/odata/filter/tree.rb +106 -52
  32. data/lib/odata/function_import.rb +168 -0
  33. data/lib/odata/model_ext.rb +639 -0
  34. data/lib/odata/navigation_attribute.rb +13 -26
  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 +20 -10
  40. data/lib/safrano.rb +18 -38
  41. data/lib/safrano/contract.rb +143 -0
  42. data/lib/safrano/core.rb +23 -107
  43. data/lib/safrano/core_ext.rb +13 -0
  44. data/lib/safrano/deprecation.rb +73 -0
  45. data/lib/safrano/multipart.rb +29 -33
  46. data/lib/safrano/rack_app.rb +66 -65
  47. data/lib/safrano/{odata_rack_builder.rb → rack_builder.rb} +18 -2
  48. data/lib/safrano/request.rb +96 -45
  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 +240 -130
  52. data/lib/safrano/version.rb +3 -1
  53. data/lib/sequel/plugins/join_by_paths.rb +6 -19
  54. metadata +32 -11
@@ -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,16 +118,18 @@ 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)
140
125
  [self, :end_with_value]
141
126
  end
142
127
 
128
+ ALLOWED_TRANSITIONS = [Safrano::TransitionEnd,
129
+ Safrano::TransitionValue].freeze
130
+
143
131
  def allowed_transitions
144
- [Safrano::TransitionEnd,
145
- Safrano::TransitionValue]
132
+ ALLOWED_TRANSITIONS
146
133
  end
147
134
  end
148
135
  include Transitions
@@ -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
data/lib/odata/select.rb CHANGED
@@ -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
data/lib/odata/walker.rb CHANGED
@@ -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
@@ -28,8 +30,12 @@ module OData
28
30
  # are $links requested ?
29
31
  attr_reader :do_links
30
32
 
33
+ NIL_SERVICE_FATAL = 'Walker is called with a nil service'
34
+ EMPTYSTR = ''
35
+ SLASH = '/'
36
+
31
37
  def initialize(service, path, content_id_refs = nil)
32
- raise 'Walker is called with a nil service' unless service
38
+ raise NIL_SERVICE_FATAL unless service
33
39
 
34
40
  path = URI.decode_www_form_component(path)
35
41
  @context = service
@@ -40,10 +46,9 @@ module OData
40
46
  @path_start = @path_remain = if service
41
47
  unprefixed(service.xpath_prefix, path)
42
48
  else # This is for batch function
43
-
44
49
  path
45
50
  end
46
- @path_done = ''
51
+ @path_done = String.new
47
52
  @status = :start
48
53
  @end_context = nil
49
54
  @do_count = nil
@@ -51,12 +56,12 @@ module OData
51
56
  end
52
57
 
53
58
  def unprefixed(prefix, path)
54
- if (prefix == '') || (prefix == '/')
59
+ if (prefix == EMPTYSTR) || (prefix == SLASH)
55
60
  path
56
61
  else
57
62
  # path.sub!(/\A#{prefix}/, '')
58
63
  # TODO check
59
- path.sub(/\A#{prefix}/, '')
64
+ path.sub(/\A#{prefix}/, EMPTYSTR)
60
65
  end
61
66
  end
62
67
 
@@ -72,6 +77,7 @@ module OData
72
77
  valid_tr = @context.allowed_transitions.select do |t|
73
78
  t.do_match(@path_remain)
74
79
  end
80
+
75
81
  # this is a very fragile and obscure but required hack (wanted: a
76
82
  # better one) to make attributes that are substrings of each other
77
83
  # work well
@@ -93,13 +99,13 @@ module OData
93
99
  @context = nil
94
100
  @status = :error
95
101
  # TODO: more appropriate error handling
96
- @error = OData::ErrorNotFound
102
+ @error = Safrano::ErrorNotFound
97
103
  end
98
104
  else
99
105
  @context = nil
100
106
  @status = :error
101
107
  # TODO: more appropriate error handling
102
- @error = OData::ErrorNotFound
108
+ @error = Safrano::ErrorNotFound
103
109
  end
104
110
  end
105
111
 
@@ -149,7 +155,7 @@ module OData
149
155
  else
150
156
  @context = nil
151
157
  @status = :error
152
- @error = OData::ErrorNotFound
158
+ @error = Safrano::ErrorNotFoundSegment.new(@path_remain)
153
159
  end
154
160
  end
155
161
  # TODO: shouldnt we raise an error here if @status != :end ?
@@ -157,5 +163,9 @@ module OData
157
163
 
158
164
  @end_context = @contexts.size >= 2 ? @contexts[-2] : @contexts[1]
159
165
  end
166
+
167
+ def finalize
168
+ (@status == :end) ? Contract.valid(@end_context) : @error
169
+ end
160
170
  end
161
171
  end