safrano 0.2.0 → 0.3.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.
- 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
|