safrano 0.2.0 → 0.3.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/lib/multipart.rb +92 -91
- data/lib/odata/attribute.rb +15 -6
- data/lib/odata/collection.rb +109 -106
- data/lib/odata/collection_filter.rb +14 -485
- data/lib/odata/collection_order.rb +15 -22
- data/lib/odata/entity.rb +31 -41
- data/lib/odata/filter/error.rb +53 -0
- data/lib/odata/filter/parse.rb +171 -0
- data/lib/odata/filter/sequel.rb +208 -0
- data/lib/odata/filter/token.rb +59 -0
- data/lib/odata/filter/tree.rb +368 -0
- data/lib/odata/relations.rb +36 -0
- data/lib/odata/url_parameters.rb +58 -0
- data/lib/odata/walker.rb +55 -42
- data/lib/request.rb +1 -3
- data/lib/safrano.rb +17 -0
- data/lib/safrano_core.rb +30 -7
- data/lib/sequel/plugins/join_by_paths.rb +239 -0
- data/lib/sequel_join_by_paths.rb +5 -0
- data/lib/service.rb +84 -112
- metadata +11 -3
@@ -1,5 +1,3 @@
|
|
1
|
-
#!/usr/bin/env ruby
|
2
|
-
|
3
1
|
require 'odata/error.rb'
|
4
2
|
|
5
3
|
# Ordering with ruby expression
|
@@ -15,13 +13,11 @@ end
|
|
15
13
|
module OData
|
16
14
|
# base class for ordering
|
17
15
|
class Order
|
18
|
-
attr_reader :assoc
|
19
|
-
attr_reader :assocs
|
20
16
|
attr_reader :oarg
|
21
|
-
def initialize(ostr,
|
17
|
+
def initialize(ostr, jh)
|
22
18
|
ostr.strip!
|
23
19
|
@orderp = ostr
|
24
|
-
@
|
20
|
+
@jh = jh
|
25
21
|
build_oarg if @orderp
|
26
22
|
end
|
27
23
|
|
@@ -31,30 +27,26 @@ module OData
|
|
31
27
|
|
32
28
|
# input : the filter string
|
33
29
|
# returns a filter object that should have a apply_to(cx) method
|
34
|
-
def self.new_by_parse(orderstr,
|
35
|
-
Order.new_full_match_complexpr(orderstr,
|
30
|
+
def self.new_by_parse(orderstr, jh)
|
31
|
+
Order.new_full_match_complexpr(orderstr, jh)
|
36
32
|
end
|
37
33
|
|
38
34
|
# handle with Sequel
|
39
|
-
def self.new_full_match_complexpr(orderstr,
|
40
|
-
ComplexOrder.new(orderstr,
|
35
|
+
def self.new_full_match_complexpr(orderstr, jh)
|
36
|
+
ComplexOrder.new(orderstr, jh)
|
41
37
|
end
|
42
38
|
|
43
39
|
def apply_to_dataset(dtcx)
|
44
40
|
dtcx
|
45
41
|
end
|
46
42
|
|
47
|
-
def apply_associations(dtcx)
|
48
|
-
@assocs.each { |aj| dtcx = dtcx.association_join(aj) }
|
49
|
-
dtcx
|
50
|
-
end
|
51
|
-
|
52
43
|
def build_oarg
|
53
44
|
field, ord = @orderp.split(' ')
|
54
45
|
oargu = if field.include?('/')
|
55
46
|
@assoc, field = field.split('/')
|
56
|
-
@
|
57
|
-
|
47
|
+
@jh.add @assoc
|
48
|
+
|
49
|
+
Sequel[@jh.start_model.get_alias_sym(@assoc)][field.strip.to_sym]
|
58
50
|
else
|
59
51
|
Sequel[field.strip.to_sym]
|
60
52
|
end
|
@@ -69,21 +61,22 @@ module OData
|
|
69
61
|
|
70
62
|
# complex ordering logic
|
71
63
|
class ComplexOrder < Order
|
72
|
-
def initialize(orderstr,
|
64
|
+
def initialize(orderstr, jh)
|
73
65
|
super
|
74
|
-
@dt = dtset
|
75
66
|
@olist = []
|
67
|
+
@jh = jh
|
76
68
|
return unless orderstr
|
77
69
|
|
78
70
|
@olist = orderstr.split(',').map do |ostr|
|
79
|
-
oo = Order.new(ostr,
|
80
|
-
@assocs.add oo.assoc if oo.assoc
|
71
|
+
oo = Order.new(ostr, @jh)
|
81
72
|
oo.oarg
|
82
73
|
end
|
83
74
|
end
|
84
75
|
|
85
76
|
def apply_to_dataset(dtcx)
|
86
|
-
@olist.each { |oarg|
|
77
|
+
@olist.each { |oarg|
|
78
|
+
dtcx = dtcx.order(oarg)
|
79
|
+
}
|
87
80
|
dtcx
|
88
81
|
end
|
89
82
|
end
|
data/lib/odata/entity.rb
CHANGED
@@ -12,12 +12,11 @@ module OData
|
|
12
12
|
# methods related to transitions to next state (cf. walker)
|
13
13
|
module Transitions
|
14
14
|
def allowed_transitions
|
15
|
-
aurgx = self.class.attribute_url_regexp
|
16
15
|
alltr = [
|
17
16
|
Safrano::TransitionEnd,
|
18
17
|
Safrano::TransitionCount,
|
19
18
|
Safrano::TransitionLinks,
|
20
|
-
Safrano::Transition.new(
|
19
|
+
Safrano::Transition.new(self.class.transition_attribute_regexp,
|
21
20
|
trans: 'transition_attribute')
|
22
21
|
]
|
23
22
|
if (ncurgx = self.class.nav_collection_url_regexp)
|
@@ -70,20 +69,17 @@ module OData
|
|
70
69
|
def nav_values
|
71
70
|
@nav_values = {}
|
72
71
|
|
73
|
-
|
74
|
-
|
75
|
-
@nav_values[na_str.to_sym] = send(na_str)
|
76
|
-
end
|
72
|
+
self.class.nav_entity_attribs&.each_key do |na_str|
|
73
|
+
@nav_values[na_str.to_sym] = send(na_str)
|
77
74
|
end
|
75
|
+
|
78
76
|
@nav_values
|
79
77
|
end
|
80
78
|
|
81
79
|
def nav_coll
|
82
80
|
@nav_coll = {}
|
83
|
-
|
84
|
-
|
85
|
-
@nav_coll[nc_str.to_sym] = send(nc_str)
|
86
|
-
end
|
81
|
+
self.class.nav_collection_attribs&.each_key do |nc_str|
|
82
|
+
@nav_coll[nc_str.to_sym] = send(nc_str)
|
87
83
|
end
|
88
84
|
@nav_coll
|
89
85
|
end
|
@@ -105,6 +101,23 @@ module OData
|
|
105
101
|
uribase: @uribase) }.to_json
|
106
102
|
end
|
107
103
|
|
104
|
+
# needed for proper datetime output
|
105
|
+
# TODO design/performance
|
106
|
+
def casted_values
|
107
|
+
# WARNING; this code is duplicated in attribute.rb
|
108
|
+
# (and the inverted transformation is in test/client.rb)
|
109
|
+
# will require a more systematic solution some day
|
110
|
+
values.transform_values! { |v|
|
111
|
+
case v
|
112
|
+
when Time
|
113
|
+
# try to get back the database time zone and value
|
114
|
+
(v + v.gmt_offset).utc.to_datetime
|
115
|
+
else
|
116
|
+
v
|
117
|
+
end
|
118
|
+
}
|
119
|
+
end
|
120
|
+
|
108
121
|
# post paylod expects the new entity in an array
|
109
122
|
def to_odata_post_json(service:)
|
110
123
|
{ 'd' => service.get_coll_odata_h(array: [self],
|
@@ -131,19 +144,17 @@ module OData
|
|
131
144
|
def odata_get(req)
|
132
145
|
copy_request_infos(req)
|
133
146
|
|
134
|
-
if req.accept?(
|
135
|
-
[200,
|
136
|
-
to_odata_json(service: req.service)]
|
147
|
+
if req.accept?(APPJSON)
|
148
|
+
[200, CT_JSON, to_odata_json(service: req.service)]
|
137
149
|
else # TODO: other formats
|
138
150
|
415
|
139
151
|
end
|
140
152
|
end
|
141
153
|
|
142
154
|
def odata_delete(req)
|
143
|
-
if req.accept?(
|
155
|
+
if req.accept?(APPJSON)
|
144
156
|
delete
|
145
|
-
[200, { '
|
146
|
-
{ 'd' => req.service.get_emptycoll_odata_h }.to_json]
|
157
|
+
[200, CT_JSON, { 'd' => req.service.get_emptycoll_odata_h }.to_json]
|
147
158
|
else # TODO: other formats
|
148
159
|
415
|
149
160
|
end
|
@@ -153,7 +164,7 @@ module OData
|
|
153
164
|
data = JSON.parse(req.body.read)
|
154
165
|
@uribase = req.uribase
|
155
166
|
|
156
|
-
if req.accept?(
|
167
|
+
if req.accept?(APPJSON)
|
157
168
|
data.delete('__metadata')
|
158
169
|
|
159
170
|
if req.in_changeset
|
@@ -194,21 +205,6 @@ module OData
|
|
194
205
|
# patch should return 204 + no content
|
195
206
|
[204, {}, []]
|
196
207
|
end
|
197
|
-
|
198
|
-
# if ( req.content_type == 'application/json' )
|
199
|
-
## Parse json payload
|
200
|
-
# begin
|
201
|
-
# data = JSON.parse(req.body.read)
|
202
|
-
# rescue JSON::ParserError => e
|
203
|
-
# return [400, {}, ['JSON Parser Error while parsing payload : ',
|
204
|
-
# e.message]]
|
205
|
-
|
206
|
-
# end
|
207
|
-
|
208
|
-
# else # TODO: other formats
|
209
|
-
|
210
|
-
# [415, {}, []]
|
211
|
-
# end
|
212
208
|
end
|
213
209
|
|
214
210
|
# redefinitions of the main methods for a navigated collection
|
@@ -227,18 +223,12 @@ module OData
|
|
227
223
|
true
|
228
224
|
end
|
229
225
|
|
230
|
-
def
|
226
|
+
def dataset
|
231
227
|
@child_dataset_method.call
|
232
228
|
end
|
233
229
|
|
234
|
-
|
235
|
-
|
236
|
-
def [](*args)
|
237
|
-
y = @child_method.call
|
238
|
-
return nil unless (found = super(args))
|
239
|
-
|
240
|
-
# y.find { |e| e.pk_val == found.pk_val }
|
241
|
-
y.find { |e| e.values == found.values }
|
230
|
+
def navigated_dataset
|
231
|
+
@child_dataset_method.call
|
242
232
|
end
|
243
233
|
|
244
234
|
def each
|
@@ -0,0 +1,53 @@
|
|
1
|
+
module OData
|
2
|
+
module Filter
|
3
|
+
class Parser
|
4
|
+
# Parser errors
|
5
|
+
class Error < StandardError
|
6
|
+
attr_reader :tok
|
7
|
+
attr_reader :typ
|
8
|
+
attr_reader :cur_val
|
9
|
+
attr_reader :cur_typ
|
10
|
+
def initialize(tok, typ, cur)
|
11
|
+
@tok = tok
|
12
|
+
@typ = typ
|
13
|
+
return unless cur
|
14
|
+
|
15
|
+
@cur_val = cur.value
|
16
|
+
@cur_typ = cur.class
|
17
|
+
end
|
18
|
+
end
|
19
|
+
# Invalid Tokens
|
20
|
+
class ErrorInvalidToken < Error
|
21
|
+
end
|
22
|
+
# Unmached closed
|
23
|
+
class ErrorUnmatchedClose < Error
|
24
|
+
end
|
25
|
+
|
26
|
+
class ErrorFunctionArgumentType < StandardError
|
27
|
+
end
|
28
|
+
|
29
|
+
class ErrorWrongColumnName < StandardError
|
30
|
+
end
|
31
|
+
|
32
|
+
# Invalid function arity
|
33
|
+
class ErrorInvalidArity < Error
|
34
|
+
end
|
35
|
+
# Invalid separator in this context (missing parenthesis?)
|
36
|
+
class ErrorInvalidSeparator < Error
|
37
|
+
end
|
38
|
+
|
39
|
+
# unmatched quot3
|
40
|
+
class UnmatchedQuoteError < Error
|
41
|
+
end
|
42
|
+
|
43
|
+
# wrong type of function argument
|
44
|
+
class ErrorInvalidArgumentType < StandardError
|
45
|
+
def initialize(tree, expected:, actual:)
|
46
|
+
@tree = tree
|
47
|
+
@expected = expected
|
48
|
+
@actual = actual
|
49
|
+
end
|
50
|
+
end
|
51
|
+
end
|
52
|
+
end
|
53
|
+
end
|
@@ -0,0 +1,171 @@
|
|
1
|
+
require_relative './token.rb'
|
2
|
+
require_relative './tree.rb'
|
3
|
+
require_relative './error.rb'
|
4
|
+
|
5
|
+
# top level OData namespace
|
6
|
+
module OData
|
7
|
+
# for handling $filter
|
8
|
+
module Filter
|
9
|
+
# Parser for $filter input
|
10
|
+
class Parser
|
11
|
+
include Token
|
12
|
+
attr_reader :cursor
|
13
|
+
def initialize(input)
|
14
|
+
@tree = RootTree.new
|
15
|
+
@cursor = @tree
|
16
|
+
@input = input
|
17
|
+
@stack = []
|
18
|
+
@binop_stack = []
|
19
|
+
end
|
20
|
+
|
21
|
+
def grow_at_cursor(child)
|
22
|
+
raise 'unknown BroGrammingError' if @cursor.nil?
|
23
|
+
|
24
|
+
@cursor.attach(child)
|
25
|
+
@cursor = child
|
26
|
+
end
|
27
|
+
|
28
|
+
def cursor_at_parent
|
29
|
+
@cursor = @cursor.parent
|
30
|
+
end
|
31
|
+
|
32
|
+
# detach cursor from parent and move cursor to the parent
|
33
|
+
# return the detached subtree
|
34
|
+
def detach_cursor
|
35
|
+
left = @cursor
|
36
|
+
cursor_at_parent
|
37
|
+
@cursor.detach(left)
|
38
|
+
end
|
39
|
+
|
40
|
+
def insert_before_cursor(node)
|
41
|
+
left = detach_cursor
|
42
|
+
grow_at_cursor(node)
|
43
|
+
@cursor.attach(left)
|
44
|
+
end
|
45
|
+
|
46
|
+
def invalid_separator_error(tok, typ)
|
47
|
+
@error = ErrorInvalidSeparator.new(tok, typ, @cursor)
|
48
|
+
end
|
49
|
+
|
50
|
+
def unmatched_quote_error(tok, typ)
|
51
|
+
@error = UnmatchedQuoteError.new(tok, typ, @cursor)
|
52
|
+
end
|
53
|
+
|
54
|
+
def invalid_closing_delimiter_error(tok, typ)
|
55
|
+
@error = ErrorUnmatchedClose.new(tok, typ, @cursor)
|
56
|
+
end
|
57
|
+
|
58
|
+
def with_accepted(tok, typ)
|
59
|
+
acc, err = @cursor.accept?(tok, typ)
|
60
|
+
if acc
|
61
|
+
yield
|
62
|
+
else
|
63
|
+
@error = err
|
64
|
+
end
|
65
|
+
end
|
66
|
+
|
67
|
+
def build
|
68
|
+
each_typed_token(@input) do |tok, typ|
|
69
|
+
case typ
|
70
|
+
when :FuncTree
|
71
|
+
with_accepted(tok, typ) do
|
72
|
+
grow_at_cursor(FuncTree.new(tok))
|
73
|
+
end
|
74
|
+
when :Delimiter
|
75
|
+
case tok
|
76
|
+
when '('
|
77
|
+
with_accepted(tok, typ) do
|
78
|
+
unless @cursor.is_a? FuncTree
|
79
|
+
grow_at_cursor(IdentityFuncTree.new)
|
80
|
+
end
|
81
|
+
openarg = ArgTree.new('(')
|
82
|
+
@stack << openarg
|
83
|
+
grow_at_cursor(openarg)
|
84
|
+
end
|
85
|
+
when ')'
|
86
|
+
unless (@cursor = @stack.pop)
|
87
|
+
break invalid_closing_delimiter_error(tok, typ)
|
88
|
+
end
|
89
|
+
|
90
|
+
with_accepted(tok, typ) do
|
91
|
+
@cursor.update_state(tok, typ)
|
92
|
+
cursor_at_parent
|
93
|
+
end
|
94
|
+
end
|
95
|
+
|
96
|
+
when :Separator
|
97
|
+
unless (@cursor = @stack.last)
|
98
|
+
break invalid_separator_error(tok, typ)
|
99
|
+
end
|
100
|
+
|
101
|
+
with_accepted(tok, typ) { @cursor.update_state(tok, typ) }
|
102
|
+
|
103
|
+
when :UnopTree
|
104
|
+
unoptr = UnopTree.new(tok)
|
105
|
+
if (prev = @binop_stack.last)
|
106
|
+
# handling of lower precedence binding vs the other
|
107
|
+
# ones(le,gt,eq...)
|
108
|
+
unless prev.precedence < unoptr.precedence
|
109
|
+
@cursor = @binop_stack.pop
|
110
|
+
@binop_stack << unoptr
|
111
|
+
end
|
112
|
+
else
|
113
|
+
@binop_stack << unoptr
|
114
|
+
end
|
115
|
+
grow_at_cursor(unoptr)
|
116
|
+
|
117
|
+
when :BinopTree
|
118
|
+
with_accepted(tok, typ) do
|
119
|
+
binoptr = BinopTree.new(tok)
|
120
|
+
if (prev = @binop_stack.last)
|
121
|
+
# handling of lower precedence binding vs the other
|
122
|
+
# ones(le,gt,eq...)
|
123
|
+
unless prev.precedence < binoptr.precedence
|
124
|
+
@cursor = @binop_stack.pop
|
125
|
+
@binop_stack << binoptr
|
126
|
+
end
|
127
|
+
else
|
128
|
+
@binop_stack << binoptr
|
129
|
+
end
|
130
|
+
insert_before_cursor(binoptr)
|
131
|
+
end
|
132
|
+
when :Literal
|
133
|
+
with_accepted(tok, typ) do
|
134
|
+
@cursor.update_state(tok, typ)
|
135
|
+
grow_at_cursor(Literal.new(tok))
|
136
|
+
end
|
137
|
+
|
138
|
+
when :Qualit
|
139
|
+
with_accepted(tok, typ) do
|
140
|
+
@cursor.update_state(tok, typ)
|
141
|
+
grow_at_cursor(Qualit.new(tok))
|
142
|
+
end
|
143
|
+
|
144
|
+
when :QString
|
145
|
+
with_accepted(tok, typ) do
|
146
|
+
@cursor.update_state(tok, typ)
|
147
|
+
grow_at_cursor(QString.new(tok))
|
148
|
+
end
|
149
|
+
|
150
|
+
when :FPNumber
|
151
|
+
with_accepted(tok, typ) do
|
152
|
+
@cursor.update_state(tok, typ)
|
153
|
+
grow_at_cursor(FPNumber.new(tok))
|
154
|
+
end
|
155
|
+
when :unmatchedQuote
|
156
|
+
break unmatched_quote_error(tok, typ)
|
157
|
+
else
|
158
|
+
raise 'Severe Error'
|
159
|
+
end
|
160
|
+
break if @error
|
161
|
+
end
|
162
|
+
begin
|
163
|
+
@tree.check_types unless @error
|
164
|
+
rescue ErrorInvalidArgumentType => e
|
165
|
+
@error = e
|
166
|
+
end
|
167
|
+
@error || @tree
|
168
|
+
end
|
169
|
+
end
|
170
|
+
end
|
171
|
+
end
|