safrano 0.4.0 → 0.4.5

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 +15 -10
  14. data/lib/odata/batch.rb +15 -13
  15. data/lib/odata/collection.rb +144 -535
  16. data/lib/odata/collection_filter.rb +47 -40
  17. data/lib/odata/collection_media.rb +145 -74
  18. data/lib/odata/collection_order.rb +50 -37
  19. data/lib/odata/common_logger.rb +36 -34
  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 +151 -197
  23. data/lib/odata/error.rb +175 -32
  24. data/lib/odata/expand.rb +126 -0
  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 +44 -36
  28. data/lib/odata/filter/sequel.rb +136 -67
  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 +113 -63
  32. data/lib/odata/function_import.rb +168 -0
  33. data/lib/odata/model_ext.rb +637 -0
  34. data/lib/odata/navigation_attribute.rb +44 -61
  35. data/lib/odata/relations.rb +5 -5
  36. data/lib/odata/select.rb +54 -0
  37. data/lib/odata/transition.rb +71 -0
  38. data/lib/odata/url_parameters.rb +128 -37
  39. data/lib/odata/walker.rb +19 -11
  40. data/lib/safrano.rb +17 -37
  41. data/lib/safrano/contract.rb +143 -0
  42. data/lib/safrano/core.rb +29 -104
  43. data/lib/safrano/core_ext.rb +13 -0
  44. data/lib/safrano/deprecation.rb +73 -0
  45. data/lib/safrano/multipart.rb +39 -43
  46. data/lib/safrano/rack_app.rb +68 -67
  47. data/lib/safrano/{odata_rack_builder.rb → rack_builder.rb} +18 -2
  48. data/lib/safrano/request.rb +102 -51
  49. data/lib/safrano/response.rb +5 -3
  50. data/lib/safrano/sequel_join_by_paths.rb +2 -2
  51. data/lib/safrano/service.rb +264 -220
  52. data/lib/safrano/version.rb +3 -1
  53. data/lib/sequel/plugins/join_by_paths.rb +17 -29
  54. metadata +34 -12
@@ -1,33 +1,31 @@
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
6
-
7
- # remove the relation between entity and parent by clearing
7
+ module Safrano
8
+ # remove the relation between entity and parent by clearing
8
9
  # the FK field(s) (if allowed)
9
- def OData.remove_nav_relation(entity, assoc, parent)
10
+ def self.remove_nav_relation(assoc, parent)
10
11
  return unless assoc
11
12
 
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
13
+ return unless assoc[:type] == :many_to_one
14
+
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 do |lk|
21
+ parent.set(lk => nil)
22
+ parent.save(transaction: false)
23
+ end
26
24
  end
27
-
25
+
28
26
  # link newly created entities(child) to an existing parent
29
27
  # by following the association_reflection rules
30
- def OData.create_nav_relation(child, assoc, parent)
28
+ def self.create_nav_relation(child, assoc, parent)
31
29
  return unless assoc
32
30
 
33
31
  # Note: this coding shares some bits from our sequel/plugins/join_by_paths,
@@ -46,16 +44,16 @@ module OData
46
44
  lks = [leftm.primary_key].flatten
47
45
  rks = [assoc[:key]].flatten
48
46
  join_cond = rks.zip(lks).to_h
49
- join_cond.each { |rk, lk|
47
+ join_cond.each do |rk, lk|
50
48
  if child.values[rk] # FK in new entity from payload not nil, only check consistency
51
49
  # with the parent - id(s)
52
- if (child.values[rk] != parent.pk_hash[lk]) # error...
53
- # TODO
54
- end
50
+ # if (child.values[rk] != parent.pk_hash[lk]) # error...
51
+ # TODO
52
+ # end
55
53
  else # we can set the FK value, thus creating the "link"
56
54
  child.set(rk => parent.pk_hash[lk])
57
55
  end
58
- }
56
+ end
59
57
  when :many_to_one
60
58
  # sets the FK values in parent to corresponding related child key-values
61
59
  # thus creating the "link" between the new entity and the parent
@@ -64,29 +62,15 @@ module OData
64
62
  lks = [assoc[:key]].flatten
65
63
  rks = [child.class.primary_key].flatten
66
64
  join_cond = rks.zip(lks).to_h
67
- join_cond.each { |rk, lk|
65
+ join_cond.each do |rk, lk|
68
66
  if parent.values[lk] # FK in parent not nil, only check consistency
69
67
  # with the child - id(s)
70
- if (parent.values[lk] != child.pk_hash[rk]) # error...
71
- # TODO
72
- end
68
+ # if parent.values[lk] != child.pk_hash[rk] # error...
69
+ # TODO
70
+ # end
73
71
  else # we can set the FK value, thus creating the "link"
74
72
  parent.set(lk => child.pk_hash[rk])
75
73
  end
76
- }
77
- end
78
- end
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
74
  end
91
75
  end
92
76
  end
@@ -94,12 +78,12 @@ module OData
94
78
  # Represents a named but nil-valued navigation-attribute of an Entity
95
79
  # (usually resulting from a NULL FK db value)
96
80
  class NilNavigationAttribute
97
- include EntityBase::NavigationInfo
81
+ include Safrano::NavigationInfo
98
82
  def odata_get(req)
99
83
  if req.walker.media_value
100
- OData::ErrorNotFound.odata_get
84
+ Safrano::ErrorNotFound.odata_get
101
85
  elsif req.accept?(APPJSON)
102
- [200, CT_JSON, to_odata_json(service: req.service)]
86
+ [200, EMPTY_HASH, to_odata_json(service: req.service)]
103
87
  else # TODO: other formats
104
88
  415
105
89
  end
@@ -108,25 +92,22 @@ module OData
108
92
  # create the nav. entity
109
93
  def odata_post(req)
110
94
  # delegate to the class method
111
- @nav_klass.odata_create_entity_and_relation(req,
112
- @navattr_reflection,
95
+ @nav_klass.odata_create_entity_and_relation(req,
96
+ @navattr_reflection,
113
97
  @nav_parent)
114
98
  end
115
99
 
116
100
  # create the nav. entity
117
101
  def odata_put(req)
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
102
+ # delegate to the class method
103
+ @nav_klass.odata_create_entity_and_relation(req,
104
+ @navattr_reflection,
105
+ @nav_parent)
125
106
  end
126
107
 
127
108
  # empty output as OData json (v2)
128
109
  def to_odata_json(*)
129
- { 'd' => {} }.to_json
110
+ { 'd' => EMPTY_HASH }.to_json
130
111
  end
131
112
 
132
113
  # for testing purpose (assert_equal ...)
@@ -137,16 +118,18 @@ module OData
137
118
  # methods related to transitions to next state (cf. walker)
138
119
  module Transitions
139
120
  def transition_end(_match_result)
140
- [nil, :end]
121
+ Safrano::Transition::RESULT_END
141
122
  end
142
123
 
143
124
  def transition_value(_match_result)
144
125
  [self, :end_with_value]
145
126
  end
146
127
 
128
+ ALLOWED_TRANSITIONS = [Safrano::TransitionEnd,
129
+ Safrano::TransitionValue].freeze
130
+
147
131
  def allowed_transitions
148
- [Safrano::TransitionEnd,
149
- Safrano::TransitionValue]
132
+ ALLOWED_TRANSITIONS
150
133
  end
151
134
  end
152
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
@@ -0,0 +1,54 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'odata/error'
4
+
5
+ # all dataset selecting related classes in our OData module
6
+ # ie do eager loading
7
+ module Safrano
8
+ # base class for selecting. We have to distinguish between
9
+ # fields of the current entity, and the navigation properties
10
+ # we can have one special case
11
+ # empty, ie no $select specified --> return all fields and all nav props
12
+ # ==> SelectAll
13
+
14
+ class SelectBase
15
+ ALL = new # re-useable selecting-all handler
16
+
17
+ def self.factory(selectstr, model)
18
+ case selectstr&.strip
19
+ when nil, '', '*'
20
+ ALL
21
+ else
22
+ Select.new(selectstr, model)
23
+ end
24
+ end
25
+
26
+ def all_props?
27
+ false
28
+ end
29
+
30
+ def ALL.all_props?
31
+ true
32
+ end
33
+
34
+ def parse_error?
35
+ Contract::OK
36
+ end
37
+ end
38
+
39
+ # single select
40
+ class Select < SelectBase
41
+ COMASPLIT = /\s*,\s*/.freeze
42
+ attr_reader :props
43
+
44
+ def initialize(selstr, model)
45
+ @model = model
46
+ @selectp = selstr.strip
47
+ @props = @selectp.split(COMASPLIT)
48
+ end
49
+
50
+ def parse_error?
51
+ (invalids = @model.find_invalid_props(props.to_set)) ? BadRequestSelectInvalidProps.new(@model, invalids) : Contract::OK
52
+ end
53
+ end
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,58 +1,149 @@
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
6
- class UrlParameters
7
+ module Safrano
8
+ class UrlParametersBase
9
+ attr_reader :expand
10
+ attr_reader :select
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 ||
16
+ def check_expand
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
31
+ end
32
+ end
33
+
34
+ # url parameters for a single entity expand/select
35
+ class UrlParameters4Single < UrlParametersBase
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
48
+ end
49
+ end
50
+
51
+ # url parameters for a collection expand/select + filter/order
52
+ class UrlParameters4Coll < UrlParametersBase
7
53
  attr_reader :filt
8
54
  attr_reader :ordby
9
- def initialize(jh, params)
10
- @jh = jh
55
+
56
+ def initialize(dataset, params = {})
57
+ super
58
+ # join helper is only needed for odering or filtering
59
+ @jh = @model.join_by_paths_helper if params['$orderby'] || params['$filter']
11
60
  @params = params
61
+ @ordby = OrderBase.factory(@params['$orderby'], @jh)
62
+ @filt = FilterBase.factory(@params['$filter'])
63
+ @expand = ExpandBase.factory(@params['$expand'], @model)
64
+ @select = SelectBase.factory(@params['$select'], @model)
12
65
  end
13
66
 
14
- def check_filter
15
- return unless @params['$filter']
67
+ def check_top
68
+ return Contract::OK unless @params['$top']
16
69
 
17
- @filt = FilterByParse.new(@params['$filter'], @jh)
18
- return BadRequestFilterParseError if @filt.parse_error?
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']
19
76
 
20
- # nil is the expected return for no errors
21
- nil
77
+ iskip = number_or_nil(@params['$skip'])
78
+ (iskip.nil? || iskip.negative?) ? BadRequestError : Contract::OK
22
79
  end
23
80
 
24
- def check_order
25
- return unless @params['$orderby']
81
+ def check_inlinecount
82
+ return Contract::OK unless (icp = @params['$inlinecount'])
26
83
 
27
- pordlist = @params['$orderby'].dup
28
- pordlist.split(',').each do |pord|
29
- pord.strip!
30
- qualfn, dir = pord.split(/\s/)
31
- qualfn.strip!
32
- dir.strip! if dir
33
- return BadRequestError unless @jh.start_model.attrib_path_valid? qualfn
34
- return BadRequestError unless [nil, 'asc', 'desc'].include? dir
35
- end
84
+ ((icp == 'allpages') || (icp == 'none')) ? Contract::OK : BadRequestInlineCountParamError
85
+ end
86
+
87
+ def check_filter
88
+ (err = @filt.parse_error?) ? err : Contract::OK
89
+ end
36
90
 
37
- @ordby = Order.new_by_parse(@params['$orderby'], @jh)
91
+ def check_orderby
92
+ return Contract::OK if @ordby.empty?
93
+ return BadRequestOrderParseError if @ordby.parse_error?
38
94
 
39
- # nil is the expected return for no errors
40
- nil
95
+ Contract::OK
41
96
  end
42
97
 
43
98
  def apply_to_dataset(dtcx)
44
- return dtcx if (@filt.nil? && @ordby.nil?)
45
-
46
- if @filt.nil?
47
- dtcx = @jh.dataset.select_all(@jh.start_model.table_name)
48
-
49
- @ordby.apply_to_dataset(dtcx)
50
- elsif @ordby.nil?
51
- @filt.apply_to_dataset(dtcx)
52
- else
53
- filtexpr = @filt.sequel_expr
54
- dtcx = @jh.dataset(dtcx).where(filtexpr).select_all(@jh.start_model.table_name)
55
- @ordby.apply_to_dataset(dtcx)
99
+ apply_expand_to_dataset(dtcx).if_valid do |dataset|
100
+ apply_filter_order_to_dataset(dataset)
101
+ end
102
+ end
103
+
104
+ def apply_expand_to_dataset(dtcx)
105
+ return Contract.valid(dtcx) if @expand.empty?
106
+
107
+ @expand.apply_to_dataset(dtcx)
108
+ end
109
+
110
+ # Warning, the @ordby and @filt objects are coupled by way of the join helper
111
+ def apply_filter_order_to_dataset(dtcx)
112
+ return Contract.valid(dtcx) if @filt.empty? && @ordby.empty?
113
+
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)
119
+
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
127
+
128
+ ###########################################################
129
+ def check_all
130
+ return Contract::OK unless @params
131
+
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
56
147
  end
57
148
  end
58
149
  end