safrano 0.3.3 → 0.4.3

Sign up to get free protection for your applications and to get access to all the features.
Files changed (36) hide show
  1. checksums.yaml +4 -4
  2. data/lib/odata/attribute.rb +9 -8
  3. data/lib/odata/batch.rb +8 -8
  4. data/lib/odata/collection.rb +239 -92
  5. data/lib/odata/collection_filter.rb +40 -9
  6. data/lib/odata/collection_media.rb +159 -28
  7. data/lib/odata/collection_order.rb +46 -36
  8. data/lib/odata/common_logger.rb +37 -12
  9. data/lib/odata/entity.rb +188 -99
  10. data/lib/odata/error.rb +60 -12
  11. data/lib/odata/expand.rb +123 -0
  12. data/lib/odata/filter/base.rb +66 -0
  13. data/lib/odata/filter/error.rb +33 -0
  14. data/lib/odata/filter/parse.rb +6 -12
  15. data/lib/odata/filter/sequel.rb +42 -29
  16. data/lib/odata/filter/sequel_function_adapter.rb +147 -0
  17. data/lib/odata/filter/token.rb +5 -1
  18. data/lib/odata/filter/tree.rb +45 -29
  19. data/lib/odata/navigation_attribute.rb +60 -27
  20. data/lib/odata/relations.rb +2 -2
  21. data/lib/odata/select.rb +42 -0
  22. data/lib/odata/url_parameters.rb +51 -36
  23. data/lib/odata/walker.rb +6 -6
  24. data/lib/safrano.rb +23 -13
  25. data/lib/{safrano_core.rb → safrano/core.rb} +12 -4
  26. data/lib/{multipart.rb → safrano/multipart.rb} +17 -26
  27. data/lib/{odata_rack_builder.rb → safrano/odata_rack_builder.rb} +0 -1
  28. data/lib/{rack_app.rb → safrano/rack_app.rb} +12 -10
  29. data/lib/{request.rb → safrano/request.rb} +8 -14
  30. data/lib/{response.rb → safrano/response.rb} +1 -2
  31. data/lib/{sequel_join_by_paths.rb → safrano/sequel_join_by_paths.rb} +1 -1
  32. data/lib/{service.rb → safrano/service.rb} +162 -131
  33. data/lib/safrano/version.rb +3 -0
  34. data/lib/sequel/plugins/join_by_paths.rb +11 -10
  35. metadata +33 -16
  36. data/lib/version.rb +0 -4
@@ -1,5 +1,3 @@
1
- #!/usr/bin/env ruby
2
-
3
1
  require 'json'
4
2
  require 'rexml/document'
5
3
  require 'safrano.rb'
@@ -31,8 +29,8 @@ module OData
31
29
  end
32
30
  end
33
31
 
34
- # base module for HTTP errors
35
- module Error
32
+ # base module for HTTP errors, when used as a Error Class
33
+ module ErrorClass
36
34
  def odata_get(req)
37
35
  if req.accept?(APPJSON)
38
36
  [const_get(:HTTP_CODE), CT_JSON,
@@ -42,9 +40,22 @@ module OData
42
40
  end
43
41
  end
44
42
  end
43
+
44
+ # base module for HTTP errors, when used as an Error instance
45
+ module ErrorInstance
46
+ def odata_get(req)
47
+ if req.accept?(APPJSON)
48
+ [self.class.const_get(:HTTP_CODE), CT_JSON,
49
+ { 'odata.error' => { 'code' => '', 'message' => @msg } }.to_json]
50
+ else
51
+ [self.class.const_get(:HTTP_CODE), CT_TEXT, @msg]
52
+ end
53
+ end
54
+ end
55
+
45
56
  # http Bad Req.
46
57
  class BadRequestError
47
- extend Error
58
+ extend ErrorClass
48
59
  HTTP_CODE = 400
49
60
  @msg = 'Bad Request Error'
50
61
  end
@@ -59,12 +70,31 @@ module OData
59
70
  HTTP_CODE = 400
60
71
  @msg = 'Bad Request: $value request for a non-media entity'
61
72
  end
73
+ class BadRequestSequelAdapterError < BadRequestError
74
+ include ErrorInstance
75
+ def initialize(err)
76
+ @msg = err.inner.message
77
+ end
78
+ end
62
79
 
63
80
  # for Syntax error in Filtering
64
81
  class BadRequestFilterParseError < BadRequestError
65
82
  HTTP_CODE = 400
66
- @msg = 'Bad Request: Syntax error in Filter'
83
+ @msg = 'Bad Request: Syntax error in $filter'
67
84
  end
85
+
86
+ # for Syntax error in $expand param
87
+ class BadRequestExpandParseError < BadRequestError
88
+ HTTP_CODE = 400
89
+ @msg = 'Bad Request: Syntax error in $expand'
90
+ end
91
+
92
+ # for Syntax error in $orderby param
93
+ class BadRequestOrderParseError < BadRequestError
94
+ HTTP_CODE = 400
95
+ @msg = 'Bad Request: Syntax error in $orderby'
96
+ end
97
+
68
98
  # for $inlinecount error
69
99
  class BadRequestInlineCountParamError < BadRequestError
70
100
  HTTP_CODE = 400
@@ -72,36 +102,54 @@ module OData
72
102
  end
73
103
  # http not found
74
104
  class ErrorNotFound
75
- extend Error
105
+ extend ErrorClass
76
106
  HTTP_CODE = 404
77
107
  @msg = 'The requested ressource was not found'
78
108
  end
79
109
  # Transition error (Safrano specific)
80
110
  class ServerTransitionError
81
- extend Error
111
+ extend ErrorClass
82
112
  HTTP_CODE = 500
83
113
  @msg = 'Server error: Segment could not be parsed'
84
114
  end
85
115
  # generic http 500 server err
86
116
  class ServerError
87
- extend Error
117
+ extend ErrorClass
88
118
  HTTP_CODE = 500
89
119
  @msg = 'Server error'
90
120
  end
91
121
  # not implemented (Safrano specific)
92
122
  class NotImplementedError
93
- extend Error
123
+ extend ErrorClass
94
124
  HTTP_CODE = 501
95
125
  end
96
126
  # batch not implemented (Safrano specific)
97
127
  class BatchNotImplementedError
98
- extend Error
128
+ extend ErrorClass
99
129
  HTTP_CODE = 501
100
130
  @msg = 'Not implemented: OData batch'
101
131
  end
132
+
102
133
  # error in filter parsing (Safrano specific)
103
134
  class FilterParseError < BadRequestError
104
- extend Error
135
+ extend ErrorClass
105
136
  HTTP_CODE = 400
106
137
  end
138
+
139
+ class FilterFunctionNotImplementedError < BadRequestError
140
+ extend ErrorClass
141
+ include ErrorInstance
142
+ @msg = 'the requested $filter function is Not implemented'
143
+ HTTP_CODE = 400
144
+ def initialize(exception)
145
+ @msg = exception.to_s
146
+ end
147
+ end
148
+ class FilterInvalidFunctionError < BadRequestError
149
+ include ErrorInstance
150
+ HTTP_CODE = 400
151
+ def initialize(exception)
152
+ @msg = exception.to_s
153
+ end
154
+ end
107
155
  end
@@ -0,0 +1,123 @@
1
+ require 'odata/error.rb'
2
+
3
+ # all dataset expanding related classes in our OData module
4
+ # ie do eager loading
5
+ module OData
6
+ # base class for expanding
7
+ class ExpandBase
8
+ EmptyExpand = new # re-useable empty expanding (idempotent)
9
+ EMPTYH = {}.freeze
10
+
11
+ def self.factory(expandstr)
12
+ expandstr.nil? ? EmptyExpand : MultiExpand.new(expandstr)
13
+ end
14
+
15
+ # output template
16
+ attr_reader :template
17
+
18
+ def apply_to_dataset(dtcx)
19
+ dtcx
20
+ end
21
+
22
+ def empty?
23
+ true
24
+ end
25
+
26
+ def parse_error?
27
+ false
28
+ end
29
+
30
+ def template
31
+ EMPTYH
32
+ end
33
+ end
34
+
35
+ # single expand
36
+ class Expand < ExpandBase
37
+ # sequel eager arg.
38
+ attr_reader :arg
39
+ attr_reader :template
40
+
41
+ # used for Sequel eager argument
42
+ # Recursive array to deep hash
43
+ # [1,2,3,4] --> {1=>{2=>{3=>4}}}
44
+ # [1] --> 1
45
+ DEEPH_0 = ->(inp) { inp.size > 1 ? { inp[0] => DEEPH_0.call(inp[1..-1]) } : inp[0] }
46
+
47
+ # used for building output template
48
+ # Recursive array to deep hash
49
+ # [1,2,3,4] --> {1=>{2=>{3=>4}}}
50
+ # [1] --> { 1 => {} }
51
+ DEEPH_1 = ->(inp) { inp.size > 1 ? { inp[0] => DEEPH_1.call(inp[1..-1]) } : { inp[0] => {} } }
52
+
53
+ NODESEP = '/'.freeze
54
+
55
+ def initialize(exstr)
56
+ exstr.strip!
57
+ @expandp = exstr
58
+ @nodes = @expandp.split(NODESEP)
59
+ build_arg
60
+ end
61
+
62
+ def apply_to_dataset(dtcx)
63
+ dtcx
64
+ end
65
+
66
+ def build_arg
67
+ # 'a/b/c/d' ==> {a: {b:{c: :d}}}
68
+ # 'xy' ==> :xy
69
+ @arg = DEEPH_0.call(@nodes.map(&:to_sym))
70
+ @template = DEEPH_1.call(@nodes)
71
+ end
72
+
73
+ def parse_error?
74
+ # todo
75
+ false
76
+ end
77
+
78
+ def empty?
79
+ false
80
+ end
81
+ end
82
+
83
+ # Multi expanding logic
84
+ class MultiExpand < ExpandBase
85
+ COMASPLIT = /\s*,\s*/.freeze
86
+ attr_reader :template
87
+
88
+ def initialize(expandstr)
89
+ expandstr.strip!
90
+ @expandp = expandstr
91
+ @exlist = []
92
+
93
+ @exlist = expandstr.split(COMASPLIT).map { |exstr| Expand.new(exstr) }
94
+ build_template
95
+ end
96
+
97
+ def apply_to_dataset(dtcx)
98
+ # use eager loading for each used association
99
+ @exlist.each { |exp| dtcx = dtcx.eager(exp.arg) }
100
+ dtcx
101
+ end
102
+
103
+ def build_template
104
+ # 'a/b/c/d,xy' ==> [ {'a' =>{ 'b' => {'c' => {'d' => {} } }}},
105
+ # { 'xy' => {} }]
106
+ #
107
+ @template = @exlist.map(&:template)
108
+
109
+ # { 'a' => { 'b' => {'c' => 'd' }},
110
+ # 'xy' => {} }
111
+ @template = @template.inject({}) { |mrg, elmt| mrg.merge elmt }
112
+ end
113
+
114
+ def parse_error?
115
+ # todo
116
+ false
117
+ end
118
+
119
+ def empty?
120
+ false
121
+ end
122
+ end
123
+ end
@@ -0,0 +1,66 @@
1
+ # frozen_string_literal: true
2
+
3
+ module OData
4
+ module Filter
5
+ # Base class for Leaves, Trees, RootTrees etc
6
+ class Node
7
+ end
8
+
9
+ # Leaves are Nodes with a parent but no children
10
+ class Leave < Node
11
+ end
12
+
13
+ # RootTrees have childrens but no parent
14
+ class RootTree < Node
15
+ end
16
+
17
+ # Tree's have Parent and children
18
+ class Tree < RootTree
19
+ end
20
+
21
+ # For functions... should have a single child---> the argument list
22
+ class FuncTree < Tree
23
+ end
24
+
25
+ # Indentity Func to use as "parent" func of parenthesis expressions
26
+ # --> allow to handle generically parenthesis always as argument of
27
+ # some function
28
+ class IdentityFuncTree < FuncTree
29
+ end
30
+
31
+ # unary op eg. NOT
32
+ class UnopTree < Tree
33
+ end
34
+
35
+ # Bin ops
36
+ class BinopTree < Tree
37
+ end
38
+
39
+ class BinopBool < BinopTree
40
+ end
41
+
42
+ class BinopArithm < BinopTree
43
+ end
44
+
45
+ # Arguments or lists
46
+ class ArgTree < Tree
47
+ end
48
+
49
+ # Numbers (floating point, ints, dec)
50
+ class FPNumber < Leave
51
+ end
52
+
53
+ # Literals are unquoted words without /
54
+ class Literal < Leave
55
+ end
56
+
57
+ # Qualit (qualified lits) are words separated by /
58
+ # path/path/path/attrib
59
+ class Qualit < Literal
60
+ end
61
+
62
+ # Quoted Strings
63
+ class QString < Leave
64
+ end
65
+ end
66
+ end
@@ -1,5 +1,28 @@
1
+ require_relative '../error'
2
+
1
3
  module OData
4
+ class SequelAdapterError < StandardError
5
+ attr_reader :inner
6
+ def initialize(err)
7
+ @inner = err
8
+ end
9
+ end
10
+
11
+ # exception to OData error bridge
12
+ module ErrorBridge
13
+ # return an odata error object wrapping the exception
14
+ # the odata error object should respond to odata_get for output
15
+ def odata_error
16
+ self.class.const_get('ODATA_ERROR_KLASS').new(self)
17
+ end
18
+ end
19
+
2
20
  module Filter
21
+ class FunctionNotImplemented < StandardError
22
+ ODATA_ERROR_KLASS = OData::FilterFunctionNotImplementedError
23
+ include ::OData::ErrorBridge
24
+ end
25
+
3
26
  class Parser
4
27
  # Parser errors
5
28
  class Error < StandardError
@@ -29,6 +52,16 @@ module OData
29
52
  class ErrorWrongColumnName < StandardError
30
53
  end
31
54
 
55
+ # attempt to add a child to a Leave
56
+ class ErrorLeaveChild < StandardError
57
+ end
58
+
59
+ # invalid function error (literal attach to IdentityFuncTree)
60
+ class ErrorInvalidFunction < StandardError
61
+ ODATA_ERROR_KLASS = OData::FilterInvalidFunctionError
62
+ include ::OData::ErrorBridge
63
+ end
64
+
32
65
  # Invalid function arity
33
66
  class ErrorInvalidArity < Error
34
67
  end
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  require_relative './token.rb'
2
4
  require_relative './tree.rb'
3
5
  require_relative './error.rb'
@@ -68,24 +70,18 @@ module OData
68
70
  each_typed_token(@input) do |tok, typ|
69
71
  case typ
70
72
  when :FuncTree
71
- with_accepted(tok, typ) do
72
- grow_at_cursor(FuncTree.new(tok))
73
- end
73
+ with_accepted(tok, typ) { grow_at_cursor(FuncTree.new(tok)) }
74
74
  when :Delimiter
75
75
  case tok
76
76
  when '('
77
77
  with_accepted(tok, typ) do
78
- unless @cursor.is_a? FuncTree
79
- grow_at_cursor(IdentityFuncTree.new)
80
- end
78
+ grow_at_cursor(IdentityFuncTree.new) unless @cursor.is_a? FuncTree
81
79
  openarg = ArgTree.new('(')
82
80
  @stack << openarg
83
81
  grow_at_cursor(openarg)
84
82
  end
85
83
  when ')'
86
- unless (@cursor = @stack.pop)
87
- break invalid_closing_delimiter_error(tok, typ)
88
- end
84
+ break invalid_closing_delimiter_error(tok, typ) unless (@cursor = @stack.pop)
89
85
 
90
86
  with_accepted(tok, typ) do
91
87
  @cursor.update_state(tok, typ)
@@ -94,9 +90,7 @@ module OData
94
90
  end
95
91
 
96
92
  when :Separator
97
- unless (@cursor = @stack.last)
98
- break invalid_separator_error(tok, typ)
99
- end
93
+ break invalid_separator_error(tok, typ) unless (@cursor = @stack.last)
100
94
 
101
95
  with_accepted(tok, typ) { @cursor.update_state(tok, typ) }
102
96
 
@@ -1,19 +1,23 @@
1
- require_relative './tree.rb'
1
+ # frozen_string_literal: true
2
+
3
+ require_relative './base.rb'
4
+ require_relative './sequel_function_adapter.rb'
5
+
2
6
  module OData
3
7
  module Filter
4
8
  # Base class for Leaves, Trees, RootTrees etc
5
- class Node
6
- end
9
+ # class Node
10
+ # end
7
11
 
8
12
  # Leaves are Nodes with a parent but no children
9
- class Leave < Node
10
- end
13
+ # class Leave < Node
14
+ # end
11
15
 
12
16
  # RootTrees have childrens but no parent
13
17
  class RootTree
14
18
  def apply_to_dataset(dtcx, jh)
15
19
  filtexpr = @children.first.leuqes(jh)
16
- dtcx = jh.dataset.where(filtexpr).select_all(jh.start_model.table_name)
20
+ jh.dataset(dtcx).where(filtexpr).select_all(jh.start_model.table_name)
17
21
  end
18
22
 
19
23
  def sequel_expr(jh)
@@ -26,6 +30,8 @@ module OData
26
30
  end
27
31
 
28
32
  # For functions... should have a single child---> the argument list
33
+ # note: Adapter specific function helpers like year() or substringof_sig2()
34
+ # need to be mixed in on startup (eg. on publish finalize)
29
35
  class FuncTree < Tree
30
36
  def leuqes(jh)
31
37
  case @value
@@ -38,28 +44,17 @@ module OData
38
44
  when :substringof
39
45
 
40
46
  # there are multiple possible argument types (but all should return edm.string)
41
- if (args[0].is_a?(QString))
47
+ if args[0].is_a?(QString)
42
48
  # substringof('Rhum', name) -->
43
49
  # name contains substr 'Rhum'
44
50
  Sequel.like(args[1].leuqes(jh),
45
51
  args[0].leuqes_substringof_sig1(jh))
46
52
  # special non standard (ui5 client) case ?
47
- elsif (args[0].is_a?(Literal) && args[1].is_a?(Literal))
53
+ elsif args[0].is_a?(Literal) && args[1].is_a?(Literal)
48
54
  Sequel.like(args[1].leuqes(jh),
49
55
  args[0].leuqes_substringof_sig1(jh))
50
- elsif (args[1].is_a?(QString))
51
- # substringof(name, '__Route du Rhum__') -->
52
- # '__Route du Rhum__' contains name as a substring
53
- # TODO... check if the database supports instr (how?)
54
- # othewise use substr(postgresql) or whatevr?
55
- instr_substr_func = if (Sequel::Model.db.adapter_scheme == :postgres)
56
- Sequel.function(:strpos, args[1].leuqes(jh), args[0].leuqes(jh))
57
- else
58
- Sequel.function(:instr, args[1].leuqes(jh), args[0].leuqes(jh))
59
- end
60
-
61
- Sequel::SQL::BooleanExpression.new(:>, instr_substr_func, 0)
62
-
56
+ elsif args[1].is_a?(QString)
57
+ substringof_sig2(jh) # adapter specific
63
58
  else
64
59
  # TODO... actually not supported?
65
60
  raise OData::Filter::Parser::ErrorFunctionArgumentType
@@ -75,6 +70,26 @@ module OData
75
70
  Sequel.function(:upper, args.first.leuqes(jh))
76
71
  when :tolower
77
72
  Sequel.function(:lower, args.first.leuqes(jh))
73
+ # all datetime funcs are adapter specific (because sqlite does not have extract)
74
+ when :year
75
+ year(jh)
76
+ when :month
77
+ month(jh)
78
+ when :second
79
+ second(jh)
80
+ when :minute
81
+ minute(jh)
82
+ when :hour
83
+ hour(jh)
84
+ when :day
85
+ day(jh)
86
+ # math functions
87
+ when :round
88
+ Sequel.function(:round, args.first.leuqes(jh))
89
+ when :floor
90
+ floor(jh)
91
+ when :ceiling
92
+ ceiling(jh)
78
93
  else
79
94
  raise OData::FilterParseError
80
95
  end
@@ -167,26 +182,24 @@ module OData
167
182
  end
168
183
 
169
184
  def leuqes_starts_like(_jh)
170
- "#{@value.to_s}%"
185
+ "#{@value}%"
171
186
  end
172
187
 
173
188
  def leuqes_ends_like(_jh)
174
- "%#{@value.to_s}"
189
+ "%#{@value}"
175
190
  end
176
191
 
177
192
  def leuqes_substringof_sig1(_jh)
178
- "%#{@value.to_s}%"
193
+ "%#{@value}%"
179
194
  end
180
195
  end
181
196
 
182
197
  # Literals are unquoted words
183
198
  class Literal
184
199
  def leuqes(jh)
185
- if jh.start_model.db_schema.has_key?(@value.to_sym)
186
- Sequel[jh.start_model.table_name][@value.to_sym]
187
- else
188
- raise OData::Filter::Parser::ErrorWrongColumnName
189
- end
200
+ raise OData::Filter::Parser::ErrorWrongColumnName unless jh.start_model.db_schema.key?(@value.to_sym)
201
+
202
+ Sequel[jh.start_model.table_name][@value.to_sym]
190
203
  end
191
204
 
192
205
  # non stantard extensions to support things like