safrano 0.4.0 → 0.4.5
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/core_ext/Dir/iter.rb +18 -0
- data/lib/core_ext/Hash/transform.rb +21 -0
- data/lib/core_ext/Integer/edm.rb +13 -0
- data/lib/core_ext/REXML/Document/output.rb +16 -0
- data/lib/core_ext/String/convert.rb +25 -0
- data/lib/core_ext/String/edm.rb +13 -0
- data/lib/core_ext/dir.rb +3 -0
- data/lib/core_ext/hash.rb +3 -0
- data/lib/core_ext/integer.rb +3 -0
- data/lib/core_ext/rexml.rb +3 -0
- data/lib/core_ext/string.rb +5 -0
- data/lib/odata/attribute.rb +15 -10
- data/lib/odata/batch.rb +15 -13
- data/lib/odata/collection.rb +144 -535
- data/lib/odata/collection_filter.rb +47 -40
- data/lib/odata/collection_media.rb +145 -74
- data/lib/odata/collection_order.rb +50 -37
- data/lib/odata/common_logger.rb +36 -34
- data/lib/odata/complex_type.rb +152 -0
- data/lib/odata/edm/primitive_types.rb +184 -0
- data/lib/odata/entity.rb +151 -197
- data/lib/odata/error.rb +175 -32
- data/lib/odata/expand.rb +126 -0
- data/lib/odata/filter/base.rb +74 -0
- data/lib/odata/filter/error.rb +49 -6
- data/lib/odata/filter/parse.rb +44 -36
- data/lib/odata/filter/sequel.rb +136 -67
- data/lib/odata/filter/sequel_function_adapter.rb +148 -0
- data/lib/odata/filter/token.rb +26 -19
- data/lib/odata/filter/tree.rb +113 -63
- data/lib/odata/function_import.rb +168 -0
- data/lib/odata/model_ext.rb +637 -0
- data/lib/odata/navigation_attribute.rb +44 -61
- data/lib/odata/relations.rb +5 -5
- data/lib/odata/select.rb +54 -0
- data/lib/odata/transition.rb +71 -0
- data/lib/odata/url_parameters.rb +128 -37
- data/lib/odata/walker.rb +19 -11
- data/lib/safrano.rb +17 -37
- data/lib/safrano/contract.rb +143 -0
- data/lib/safrano/core.rb +29 -104
- data/lib/safrano/core_ext.rb +13 -0
- data/lib/safrano/deprecation.rb +73 -0
- data/lib/safrano/multipart.rb +39 -43
- data/lib/safrano/rack_app.rb +68 -67
- data/lib/safrano/{odata_rack_builder.rb → rack_builder.rb} +18 -2
- data/lib/safrano/request.rb +102 -51
- data/lib/safrano/response.rb +5 -3
- data/lib/safrano/sequel_join_by_paths.rb +2 -2
- data/lib/safrano/service.rb +264 -220
- data/lib/safrano/version.rb +3 -1
- data/lib/sequel/plugins/join_by_paths.rb +17 -29
- metadata +34 -12
data/lib/odata/error.rb
CHANGED
@@ -1,23 +1,31 @@
|
|
1
|
-
|
1
|
+
# frozen_string_literal: true
|
2
2
|
|
3
3
|
require 'json'
|
4
4
|
require 'rexml/document'
|
5
|
-
|
5
|
+
require_relative '../safrano/contract'
|
6
6
|
|
7
7
|
# Error handling
|
8
|
-
module
|
8
|
+
module Safrano
|
9
9
|
# for errors occurring in API (publishing) usage --> Exceptions
|
10
10
|
module API
|
11
11
|
# when published class is not a Sequel Model
|
12
12
|
class ModelNameError < NameError
|
13
13
|
def initialize(name)
|
14
|
-
super("class #{name} is not a Sequel Model"
|
14
|
+
super("class #{name} is not a Sequel Model")
|
15
15
|
end
|
16
16
|
end
|
17
|
+
|
18
|
+
# when published complex type is not a Complex Type
|
19
|
+
class ComplexTypeNameError < NameError
|
20
|
+
def initialize(name)
|
21
|
+
super("class #{name} is not a ComplexType")
|
22
|
+
end
|
23
|
+
end
|
24
|
+
|
17
25
|
# when published class as media does not have the mandatory media fields
|
18
26
|
class MediaModelError < NameError
|
19
27
|
def initialize(name)
|
20
|
-
super("Model #{name} does not have the mandatory media attributes content_type/media_src"
|
28
|
+
super("Model #{name} does not have the mandatory media attributes content_type/media_src")
|
21
29
|
end
|
22
30
|
end
|
23
31
|
|
@@ -29,47 +37,101 @@ module OData
|
|
29
37
|
super(msg, symbname)
|
30
38
|
end
|
31
39
|
end
|
40
|
+
|
41
|
+
# duplicate attribute name
|
42
|
+
class ModelDuplicateAttributeError < NameError
|
43
|
+
def initialize(klass, symb)
|
44
|
+
symbname = symb.to_s
|
45
|
+
msg = "There is already an attribute :#{symbname} defined in class #{klass}"
|
46
|
+
super(msg, symbname)
|
47
|
+
end
|
48
|
+
end
|
32
49
|
end
|
33
50
|
|
34
51
|
# base module for HTTP errors, when used as a Error Class
|
35
52
|
module ErrorClass
|
53
|
+
include ::Safrano::Contract::Invalid
|
54
|
+
def http_code
|
55
|
+
const_get(:HTTP_CODE)
|
56
|
+
end
|
57
|
+
EMPTYH = {}.freeze
|
36
58
|
def odata_get(req)
|
59
|
+
message = (m = @msg.to_s).empty? ? to_s : m
|
37
60
|
if req.accept?(APPJSON)
|
38
|
-
|
39
|
-
|
61
|
+
# json is default content type so we dont need to specify it here again
|
62
|
+
[self.http_code, EMPTY_HASH,
|
63
|
+
{ 'odata.error' => { 'code' => "#{http_code}",
|
64
|
+
'type' => to_s,
|
65
|
+
'message' => message } }.to_json]
|
40
66
|
else
|
41
|
-
[
|
67
|
+
[self.http_code, CT_TEXT, message]
|
42
68
|
end
|
43
69
|
end
|
44
70
|
end
|
45
|
-
|
71
|
+
|
46
72
|
# base module for HTTP errors, when used as an Error instance
|
47
73
|
module ErrorInstance
|
74
|
+
include ::Safrano::Contract::Invalid
|
75
|
+
# can(should) be overriden in subclasses
|
76
|
+
def msg
|
77
|
+
@msg
|
78
|
+
end
|
79
|
+
|
48
80
|
def odata_get(req)
|
81
|
+
message = (m = msg.to_s).empty? ? self.class.to_s : m
|
49
82
|
if req.accept?(APPJSON)
|
50
|
-
|
51
|
-
|
83
|
+
# json is default content type so we dont need to specify it here again
|
84
|
+
[self.class.http_code, EMPTY_HASH,
|
85
|
+
{ 'odata.error' => { 'code' => "#{self.class.http_code}",
|
86
|
+
'type' => "#{self.class}",
|
87
|
+
'message' => message } }.to_json]
|
52
88
|
else
|
53
|
-
[self.class.
|
89
|
+
[self.class.http_code, CT_TEXT, message]
|
54
90
|
end
|
55
91
|
end
|
56
92
|
end
|
57
|
-
|
93
|
+
|
94
|
+
# generic http 500 server err
|
95
|
+
class ServerError
|
96
|
+
extend ErrorClass
|
97
|
+
HTTP_CODE = 500
|
98
|
+
@msg = 'Server error'
|
99
|
+
end
|
100
|
+
|
101
|
+
# for outputing Sequel exceptions that we could not prevent
|
102
|
+
class SequelExceptionError < ServerError
|
103
|
+
include ErrorInstance
|
104
|
+
def initialize(seqle7n)
|
105
|
+
@msg = seqle7n.message
|
106
|
+
end
|
107
|
+
end
|
108
|
+
|
109
|
+
# for outputing Ruby StandardError exceptions that we could not prevent
|
110
|
+
class RubyStandardErrorException < ServerError
|
111
|
+
include ErrorInstance
|
112
|
+
def initialize(rubye7n)
|
113
|
+
@msg = rubye7n.message
|
114
|
+
end
|
115
|
+
end
|
116
|
+
|
58
117
|
# http Bad Req.
|
59
118
|
class BadRequestError
|
60
119
|
extend ErrorClass
|
120
|
+
include ErrorInstance
|
61
121
|
HTTP_CODE = 400
|
62
|
-
@msg = 'Bad Request
|
122
|
+
@msg = 'Bad Request'
|
123
|
+
def initialize(reason)
|
124
|
+
@msg = "Bad Request : #{reason}"
|
125
|
+
end
|
63
126
|
end
|
127
|
+
|
64
128
|
# Generic failed changeset
|
65
129
|
class BadRequestFailedChangeSet < BadRequestError
|
66
|
-
HTTP_CODE = 400
|
67
130
|
@msg = 'Bad Request: Failed changeset '
|
68
131
|
end
|
69
132
|
|
70
133
|
# $value request for a non-media entity
|
71
134
|
class BadRequestNonMediaValue < BadRequestError
|
72
|
-
HTTP_CODE = 400
|
73
135
|
@msg = 'Bad Request: $value request for a non-media entity'
|
74
136
|
end
|
75
137
|
class BadRequestSequelAdapterError < BadRequestError
|
@@ -78,49 +140,130 @@ module OData
|
|
78
140
|
@msg = err.inner.message
|
79
141
|
end
|
80
142
|
end
|
81
|
-
|
143
|
+
|
82
144
|
# for Syntax error in Filtering
|
83
145
|
class BadRequestFilterParseError < BadRequestError
|
84
|
-
|
85
|
-
|
146
|
+
@msg = 'Bad Request: Syntax error in $filter'
|
147
|
+
end
|
148
|
+
|
149
|
+
# for invalid attribute in path
|
150
|
+
class BadRequestInvalidAttribPath < BadRequestError
|
151
|
+
include ErrorInstance
|
152
|
+
def initialize(model, attr)
|
153
|
+
@msg = "Bad Request: the attribute #{attr} is invalid for entityset #{model.entity_set_name}"
|
154
|
+
end
|
155
|
+
end
|
156
|
+
|
157
|
+
# for invalid attribute in $expand param
|
158
|
+
class BadRequestExpandInvalidPath < BadRequestError
|
159
|
+
include ErrorInstance
|
160
|
+
def initialize(model, path)
|
161
|
+
@msg = "Bad Request: the $expand path #{path} is invalid for entityset #{model.entity_set_name}"
|
162
|
+
end
|
163
|
+
end
|
164
|
+
# for invalid properti(es) in $select param
|
165
|
+
class BadRequestSelectInvalidProps < BadRequestError
|
166
|
+
include ErrorInstance
|
167
|
+
def initialize(model, iprops)
|
168
|
+
@msg = ((iprops.size > 1) ? "Bad Request: the $select properties #{iprops.to_a.join(', ')} are invalid for entityset #{model.entity_set_name}" : "Bad Request: the $select property #{iprops.first} is invalid for entityset #{model.entity_set_name}")
|
169
|
+
end
|
170
|
+
end
|
171
|
+
|
172
|
+
# for Syntax error in $orderby param
|
173
|
+
class BadRequestOrderParseError < BadRequestError
|
174
|
+
@msg = 'Bad Request: Syntax error in $orderby'
|
86
175
|
end
|
176
|
+
|
87
177
|
# for $inlinecount error
|
88
178
|
class BadRequestInlineCountParamError < BadRequestError
|
89
|
-
HTTP_CODE = 400
|
90
179
|
@msg = 'Bad Request: wrong $inlinecount parameter'
|
91
180
|
end
|
181
|
+
|
92
182
|
# http not found
|
93
183
|
class ErrorNotFound
|
94
184
|
extend ErrorClass
|
95
185
|
HTTP_CODE = 404
|
96
186
|
@msg = 'The requested ressource was not found'
|
97
187
|
end
|
188
|
+
|
189
|
+
# http not found segment
|
190
|
+
class ErrorNotFoundSegment < ErrorNotFound
|
191
|
+
include ErrorInstance
|
192
|
+
|
193
|
+
def initialize(segment)
|
194
|
+
@msg = "The requested ressource segment #{segment} was not found"
|
195
|
+
end
|
196
|
+
end
|
197
|
+
|
98
198
|
# Transition error (Safrano specific)
|
99
|
-
class ServerTransitionError
|
100
|
-
extend ErrorClass
|
101
|
-
HTTP_CODE = 500
|
199
|
+
class ServerTransitionError < ServerError
|
102
200
|
@msg = 'Server error: Segment could not be parsed'
|
103
201
|
end
|
104
|
-
|
105
|
-
class ServerError
|
106
|
-
extend ErrorClass
|
107
|
-
HTTP_CODE = 500
|
108
|
-
@msg = 'Server error'
|
109
|
-
end
|
202
|
+
|
110
203
|
# not implemented (Safrano specific)
|
111
204
|
class NotImplementedError
|
112
205
|
extend ErrorClass
|
113
206
|
HTTP_CODE = 501
|
114
207
|
end
|
115
|
-
|
116
|
-
|
208
|
+
|
209
|
+
# version not implemented (Safrano specific)
|
210
|
+
class VersionNotImplementedError
|
117
211
|
extend ErrorClass
|
118
212
|
HTTP_CODE = 501
|
213
|
+
@msg = 'The requested OData version is not yet supported'
|
214
|
+
end
|
215
|
+
# batch not implemented (Safrano specific)
|
216
|
+
class BatchNotImplementedError < NotImplementedError
|
119
217
|
@msg = 'Not implemented: OData batch'
|
120
218
|
end
|
219
|
+
|
121
220
|
# error in filter parsing (Safrano specific)
|
122
221
|
class FilterParseError < BadRequestError
|
123
222
|
extend ErrorClass
|
124
|
-
|
223
|
+
end
|
224
|
+
|
225
|
+
class FilterFunctionNotImplementedError < BadRequestError
|
226
|
+
extend ErrorClass
|
227
|
+
include ErrorInstance
|
228
|
+
@msg = 'the requested $filter function is Not implemented'
|
229
|
+
def initialize(xmsg)
|
230
|
+
@msg = xmsg
|
231
|
+
end
|
232
|
+
end
|
233
|
+
class FilterUnknownFunctionError < BadRequestError
|
234
|
+
include ErrorInstance
|
235
|
+
def initialize(badfuncname)
|
236
|
+
@msg = "Bad request: unknown function #{badfuncname} in $filter"
|
237
|
+
end
|
238
|
+
end
|
239
|
+
class FilterParseErrorWrongColumnName < BadRequestError
|
240
|
+
extend ErrorClass
|
241
|
+
@msg = 'Bad request: invalid property name in $filter'
|
242
|
+
end
|
243
|
+
class FilterParseWrappedError < BadRequestError
|
244
|
+
include ErrorInstance
|
245
|
+
def initialize(exception)
|
246
|
+
@msg = exception.to_s
|
247
|
+
end
|
248
|
+
end
|
249
|
+
class ServiceOperationParameterMissing < BadRequestError
|
250
|
+
include ErrorInstance
|
251
|
+
def initialize(missing:, sopname:)
|
252
|
+
@msg = "Bad request: Parameter(s) missing for for service operation #{sopname} : #{missing.join(', ')}"
|
253
|
+
end
|
254
|
+
end
|
255
|
+
|
256
|
+
class ServiceOperationParameterError < BadRequestError
|
257
|
+
include ErrorInstance
|
258
|
+
def initialize(type:, value:, param:, sopname:)
|
259
|
+
@type = type
|
260
|
+
@value = value
|
261
|
+
@param = param
|
262
|
+
@sopname = sopname
|
263
|
+
end
|
264
|
+
|
265
|
+
def msg
|
266
|
+
"Bad request: Parameter #{@param} with value '#{@value}' cannot be converted to type #{@type} for service operation #{@sopname}"
|
267
|
+
end
|
125
268
|
end
|
126
269
|
end
|
data/lib/odata/expand.rb
ADDED
@@ -0,0 +1,126 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'odata/error.rb'
|
4
|
+
|
5
|
+
# all dataset expanding related classes in our OData module
|
6
|
+
# ie do eager loading
|
7
|
+
module Safrano
|
8
|
+
# base class for expanding
|
9
|
+
class ExpandBase
|
10
|
+
EmptyExpand = new # re-useable empty expanding (idempotent)
|
11
|
+
EMPTYH = {}.freeze
|
12
|
+
|
13
|
+
def self.factory(expandstr, model)
|
14
|
+
expandstr.nil? ? EmptyExpand : MultiExpand.new(expandstr, model)
|
15
|
+
end
|
16
|
+
|
17
|
+
# output template
|
18
|
+
attr_reader :template
|
19
|
+
|
20
|
+
def apply_to_dataset(dtcx)
|
21
|
+
Contract.valid(dtcx)
|
22
|
+
end
|
23
|
+
|
24
|
+
def empty?
|
25
|
+
true
|
26
|
+
end
|
27
|
+
|
28
|
+
def parse_error?
|
29
|
+
Contract::OK
|
30
|
+
end
|
31
|
+
|
32
|
+
def template
|
33
|
+
EMPTYH
|
34
|
+
end
|
35
|
+
end
|
36
|
+
|
37
|
+
# single expand
|
38
|
+
class Expand < ExpandBase
|
39
|
+
# sequel eager arg.
|
40
|
+
attr_reader :arg
|
41
|
+
attr_reader :template
|
42
|
+
|
43
|
+
# used for Sequel eager argument
|
44
|
+
# Recursive array to deep hash
|
45
|
+
# [1,2,3,4] --> {1=>{2=>{3=>4}}}
|
46
|
+
# [1] --> 1
|
47
|
+
DEEPH_0 = ->(inp) { inp.size > 1 ? { inp[0] => DEEPH_0.call(inp[1..-1]) } : inp[0] }
|
48
|
+
|
49
|
+
# used for building output template
|
50
|
+
# Recursive array to deep hash
|
51
|
+
# [1,2,3,4] --> {1=>{2=>{3=>4}}}
|
52
|
+
# [1] --> { 1 => {} }
|
53
|
+
DEEPH_1 = ->(inp) { inp.size > 1 ? { inp[0] => DEEPH_1.call(inp[1..-1]) } : { inp[0] => {} } }
|
54
|
+
|
55
|
+
NODESEP = '/'.freeze
|
56
|
+
|
57
|
+
def initialize(exstr)
|
58
|
+
exstr.strip!
|
59
|
+
@expandp = exstr
|
60
|
+
@nodes = @expandp.split(NODESEP)
|
61
|
+
build_arg
|
62
|
+
end
|
63
|
+
|
64
|
+
def apply_to_dataset(dtcx)
|
65
|
+
Contract.valid(dtcx)
|
66
|
+
end
|
67
|
+
|
68
|
+
def build_arg
|
69
|
+
# 'a/b/c/d' ==> {a: {b:{c: :d}}}
|
70
|
+
# 'xy' ==> :xy
|
71
|
+
@arg = DEEPH_0.call(@nodes.map(&:to_sym))
|
72
|
+
@template = DEEPH_1.call(@nodes)
|
73
|
+
end
|
74
|
+
|
75
|
+
def empty?
|
76
|
+
false
|
77
|
+
end
|
78
|
+
end
|
79
|
+
|
80
|
+
# Multi expanding logic
|
81
|
+
class MultiExpand < ExpandBase
|
82
|
+
COMASPLIT = /\s*,\s*/.freeze
|
83
|
+
attr_reader :template
|
84
|
+
|
85
|
+
# Note: if you change this method, please also update arity_full_monkey?
|
86
|
+
# see below
|
87
|
+
def initialize(expandstr, model)
|
88
|
+
expandstr.strip!
|
89
|
+
@model = model
|
90
|
+
@expandp = expandstr
|
91
|
+
|
92
|
+
@exstrlist = expandstr.split(COMASPLIT)
|
93
|
+
@exlist = @exstrlist.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
|
+
Contract.valid(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
|
+
@exstrlist.each do |expstr|
|
116
|
+
return BadRequestExpandInvalidPath.new(@model, expstr) unless @model.expand_path_valid? expstr
|
117
|
+
end
|
118
|
+
|
119
|
+
Contract::OK
|
120
|
+
end
|
121
|
+
|
122
|
+
def empty?
|
123
|
+
false
|
124
|
+
end
|
125
|
+
end
|
126
|
+
end
|
@@ -0,0 +1,74 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Safrano
|
4
|
+
module Filter
|
5
|
+
# Base class for Leaves, Trees, RootTrees etc
|
6
|
+
class Node
|
7
|
+
def success(res)
|
8
|
+
Contract.valid(res)
|
9
|
+
end
|
10
|
+
end
|
11
|
+
|
12
|
+
# Leaves are Nodes with a parent but no children
|
13
|
+
class Leave < Node
|
14
|
+
end
|
15
|
+
|
16
|
+
# RootTrees have childrens but no parent
|
17
|
+
class RootTree < Node
|
18
|
+
end
|
19
|
+
|
20
|
+
# Tree's have Parent and children
|
21
|
+
class Tree < RootTree
|
22
|
+
end
|
23
|
+
|
24
|
+
# For functions... should have a single child---> the argument list
|
25
|
+
class FuncTree < Tree
|
26
|
+
end
|
27
|
+
|
28
|
+
# Indentity Func to use as "parent" func of parenthesis expressions
|
29
|
+
# --> allow to handle generically parenthesis always as argument of
|
30
|
+
# some function
|
31
|
+
class IdentityFuncTree < FuncTree
|
32
|
+
end
|
33
|
+
|
34
|
+
# unary op eg. NOT
|
35
|
+
class UnopTree < Tree
|
36
|
+
end
|
37
|
+
|
38
|
+
# Bin ops
|
39
|
+
class BinopTree < Tree
|
40
|
+
end
|
41
|
+
|
42
|
+
class BinopBool < BinopTree
|
43
|
+
end
|
44
|
+
|
45
|
+
class BinopArithm < BinopTree
|
46
|
+
end
|
47
|
+
|
48
|
+
# Arguments or lists
|
49
|
+
class ArgTree < Tree
|
50
|
+
end
|
51
|
+
|
52
|
+
# Numbers (floating point, ints, dec)
|
53
|
+
class FPNumber < Leave
|
54
|
+
end
|
55
|
+
|
56
|
+
# Literals are unquoted words without /
|
57
|
+
class Literal < Leave
|
58
|
+
end
|
59
|
+
|
60
|
+
# Null Literal is unquoted null word
|
61
|
+
class NullLiteral < Literal
|
62
|
+
LEUQES = nil
|
63
|
+
end
|
64
|
+
|
65
|
+
# Qualit (qualified lits) are words separated by /
|
66
|
+
# path/path/path/attrib
|
67
|
+
class Qualit < Literal
|
68
|
+
end
|
69
|
+
|
70
|
+
# Quoted Strings
|
71
|
+
class QString < Leave
|
72
|
+
end
|
73
|
+
end
|
74
|
+
end
|