safrano 0.2.0 → 0.3.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -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, _dtset)
17
+ def initialize(ostr, jh)
22
18
  ostr.strip!
23
19
  @orderp = ostr
24
- @assocs = Set.new
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, dtset = nil)
35
- Order.new_full_match_complexpr(orderstr, dtset)
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, dtset)
40
- ComplexOrder.new(orderstr, dtset)
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
- @assoc = @assoc.to_sym
57
- Sequel[@assoc][field.strip.to_sym]
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, dtset)
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, dtset)
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| dtcx = dtcx.order(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(%r{\A/(#{aurgx})(.*)\z},
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
- if self.class.nav_entity_attribs
74
- self.class.nav_entity_attribs.each_key do |na_str|
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
- if self.class.nav_collection_attribs
84
- self.class.nav_collection_attribs.each_key do |nc_str|
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?('application/json')
135
- [200, { 'Content-Type' => 'application/json;charset=utf-8' },
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?('application/json')
155
+ if req.accept?(APPJSON)
144
156
  delete
145
- [200, { 'Content-Type' => 'application/json;charset=utf-8' },
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?('application/json')
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 navigated_dataset
226
+ def dataset
231
227
  @child_dataset_method.call
232
228
  end
233
229
 
234
- # TODO: this is designed by my left foot. maybe my right one can do better
235
- # at least it does what it should (testunit passed)
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