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.
@@ -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