safrano 0.3.3 → 0.4.3

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