safrano 0.4.1 → 0.4.6

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 +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 +155 -99
  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 +183 -216
  23. data/lib/odata/error.rb +195 -31
  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 +639 -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 +20 -10
  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 +274 -219
  52. data/lib/safrano/version.rb +3 -1
  53. data/lib/sequel/plugins/join_by_paths.rb +17 -29
  54. metadata +34 -11
@@ -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