safrano 0.4.3 → 0.5.1
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/lib/core_ext/Dir/iter.rb +18 -0
- data/lib/core_ext/Hash/transform.rb +21 -0
- data/lib/core_ext/Integer/edm.rb +13 -0
- data/lib/core_ext/REXML/Document/output.rb +16 -0
- data/lib/core_ext/String/convert.rb +25 -0
- data/lib/core_ext/String/edm.rb +13 -0
- data/lib/core_ext/dir.rb +3 -0
- data/lib/core_ext/hash.rb +3 -0
- data/lib/core_ext/integer.rb +3 -0
- data/lib/core_ext/rexml.rb +3 -0
- data/lib/core_ext/string.rb +5 -0
- data/lib/odata/attribute.rb +8 -4
- data/lib/odata/batch.rb +9 -7
- data/lib/odata/collection.rb +139 -642
- data/lib/odata/collection_filter.rb +18 -42
- data/lib/odata/collection_media.rb +111 -54
- data/lib/odata/collection_order.rb +5 -2
- data/lib/odata/common_logger.rb +2 -0
- data/lib/odata/complex_type.rb +196 -0
- data/lib/odata/edm/primitive_types.rb +184 -0
- data/lib/odata/entity.rb +78 -123
- data/lib/odata/error.rb +170 -37
- data/lib/odata/expand.rb +20 -17
- data/lib/odata/filter/base.rb +9 -1
- data/lib/odata/filter/error.rb +43 -27
- data/lib/odata/filter/parse.rb +39 -25
- data/lib/odata/filter/sequel.rb +112 -56
- data/lib/odata/filter/sequel_function_adapter.rb +50 -49
- data/lib/odata/filter/token.rb +21 -18
- data/lib/odata/filter/tree.rb +78 -44
- data/lib/odata/function_import.rb +168 -0
- data/lib/odata/model_ext.rb +641 -0
- data/lib/odata/navigation_attribute.rb +9 -24
- data/lib/odata/relations.rb +5 -5
- data/lib/odata/select.rb +17 -5
- data/lib/odata/transition.rb +71 -0
- data/lib/odata/url_parameters.rb +100 -24
- data/lib/odata/walker.rb +18 -10
- data/lib/safrano.rb +18 -38
- data/lib/safrano/contract.rb +141 -0
- data/lib/safrano/core.rb +24 -106
- data/lib/safrano/core_ext.rb +13 -0
- data/lib/safrano/deprecation.rb +73 -0
- data/lib/safrano/multipart.rb +29 -24
- data/lib/safrano/rack_app.rb +62 -63
- data/lib/safrano/{odata_rack_builder.rb → rack_builder.rb} +18 -1
- data/lib/safrano/request.rb +96 -38
- data/lib/safrano/response.rb +4 -2
- data/lib/safrano/sequel_join_by_paths.rb +2 -2
- data/lib/safrano/service.rb +156 -110
- data/lib/safrano/version.rb +3 -1
- data/lib/sequel/plugins/join_by_paths.rb +6 -19
- metadata +30 -11
@@ -1,8 +1,10 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
1
3
|
require 'json'
|
2
|
-
require_relative '../safrano/core
|
3
|
-
require_relative './entity
|
4
|
+
require_relative '../safrano/core'
|
5
|
+
require_relative './entity'
|
4
6
|
|
5
|
-
module
|
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
|
81
|
+
include Safrano::NavigationInfo
|
94
82
|
def odata_get(req)
|
95
83
|
if req.walker.media_value
|
96
|
-
|
84
|
+
Safrano::ErrorNotFound.odata_get
|
97
85
|
elsif req.accept?(APPJSON)
|
98
|
-
[200,
|
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
|
-
|
121
|
+
Safrano::Transition::RESULT_END
|
137
122
|
end
|
138
123
|
|
139
124
|
def transition_value(_match_result)
|
data/lib/odata/relations.rb
CHANGED
@@ -1,9 +1,9 @@
|
|
1
|
-
|
1
|
+
# frozen_string_literal: true
|
2
2
|
|
3
3
|
require 'set'
|
4
4
|
|
5
5
|
# OData relation related classes/module
|
6
|
-
module
|
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
|
-
|
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 =
|
79
|
+
rid = Safrano::RelationManager.build_id(arg)
|
80
80
|
if @list.key?(rid)
|
81
81
|
@list[rid]
|
82
82
|
else
|
83
|
-
rel =
|
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
|
-
|
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
|
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
|
-
|
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
|
data/lib/odata/url_parameters.rb
CHANGED
@@ -1,23 +1,50 @@
|
|
1
|
-
|
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
|
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
|
-
|
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
|
-
|
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(
|
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
|
-
|
88
|
+
(err = @filt.parse_error?) ? err : Contract::OK
|
41
89
|
end
|
42
90
|
|
43
|
-
def
|
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
|
-
|
49
|
-
|
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
|
63
|
-
|
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
|
-
|
66
|
-
|
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
|
-
|
69
|
-
|
128
|
+
###########################################################
|
129
|
+
def check_all
|
130
|
+
return Contract::OK unless @params
|
70
131
|
|
71
|
-
|
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
|
5
|
+
require 'safrano'
|
4
6
|
|
5
|
-
module
|
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
|
30
|
-
|
31
|
-
|
32
|
-
|
32
|
+
|
33
|
+
NIL_SERVICE_FATAL = 'Walker is called with a nil service'
|
34
|
+
EMPTYSTR = ''
|
35
|
+
SLASH = '/'
|
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 =
|
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 =
|
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 =
|
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
|