safrano 0.4.2 → 0.5.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.
- 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 +9 -7
- data/lib/odata/collection.rb +140 -591
- data/lib/odata/collection_filter.rb +18 -42
- data/lib/odata/collection_media.rb +111 -54
- data/lib/odata/collection_order.rb +5 -2
- data/lib/odata/common_logger.rb +2 -0
- data/lib/odata/complex_type.rb +152 -0
- data/lib/odata/edm/primitive_types.rb +184 -0
- data/lib/odata/entity.rb +123 -172
- data/lib/odata/error.rb +183 -32
- data/lib/odata/expand.rb +20 -17
- data/lib/odata/filter/base.rb +74 -0
- data/lib/odata/filter/error.rb +49 -6
- data/lib/odata/filter/parse.rb +41 -25
- data/lib/odata/filter/sequel.rb +133 -62
- data/lib/odata/filter/sequel_function_adapter.rb +148 -0
- data/lib/odata/filter/token.rb +26 -19
- data/lib/odata/filter/tree.rb +106 -52
- data/lib/odata/function_import.rb +168 -0
- data/lib/odata/model_ext.rb +639 -0
- data/lib/odata/navigation_attribute.rb +13 -26
- data/lib/odata/relations.rb +5 -5
- data/lib/odata/select.rb +17 -5
- data/lib/odata/transition.rb +71 -0
- data/lib/odata/url_parameters.rb +100 -24
- data/lib/odata/walker.rb +20 -10
- data/lib/safrano.rb +18 -38
- data/lib/safrano/contract.rb +143 -0
- data/lib/safrano/core.rb +23 -107
- data/lib/safrano/core_ext.rb +13 -0
- data/lib/safrano/deprecation.rb +73 -0
- data/lib/safrano/multipart.rb +29 -33
- data/lib/safrano/rack_app.rb +66 -65
- data/lib/safrano/{odata_rack_builder.rb → rack_builder.rb} +18 -2
- data/lib/safrano/request.rb +96 -45
- data/lib/safrano/response.rb +4 -2
- data/lib/safrano/sequel_join_by_paths.rb +2 -2
- data/lib/safrano/service.rb +240 -130
- data/lib/safrano/version.rb +3 -1
- data/lib/sequel/plugins/join_by_paths.rb +6 -19
- metadata +32 -11
data/lib/odata/error.rb
CHANGED
@@ -1,21 +1,31 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
1
3
|
require 'json'
|
2
4
|
require 'rexml/document'
|
3
|
-
|
5
|
+
require_relative '../safrano/contract'
|
4
6
|
|
5
7
|
# Error handling
|
6
|
-
module
|
8
|
+
module Safrano
|
7
9
|
# for errors occurring in API (publishing) usage --> Exceptions
|
8
10
|
module API
|
9
11
|
# when published class is not a Sequel Model
|
10
12
|
class ModelNameError < NameError
|
11
13
|
def initialize(name)
|
12
|
-
super("class #{name} is not a Sequel Model"
|
14
|
+
super("class #{name} is not a Sequel Model")
|
13
15
|
end
|
14
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
|
+
|
15
25
|
# when published class as media does not have the mandatory media fields
|
16
26
|
class MediaModelError < NameError
|
17
27
|
def initialize(name)
|
18
|
-
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")
|
19
29
|
end
|
20
30
|
end
|
21
31
|
|
@@ -27,47 +37,120 @@ module OData
|
|
27
37
|
super(msg, symbname)
|
28
38
|
end
|
29
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
|
30
49
|
end
|
31
50
|
|
32
51
|
# base module for HTTP errors, when used as a Error Class
|
33
52
|
module ErrorClass
|
53
|
+
include ::Safrano::Contract::Invalid
|
54
|
+
def http_code
|
55
|
+
const_get(:HTTP_CODE)
|
56
|
+
end
|
57
|
+
EMPTYH = {}.freeze
|
34
58
|
def odata_get(req)
|
59
|
+
message = (m = @msg.to_s).empty? ? to_s : m
|
35
60
|
if req.accept?(APPJSON)
|
36
|
-
|
37
|
-
|
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]
|
38
66
|
else
|
39
|
-
[
|
67
|
+
[self.http_code, CT_TEXT, message]
|
40
68
|
end
|
41
69
|
end
|
42
70
|
end
|
43
71
|
|
44
72
|
# base module for HTTP errors, when used as an Error instance
|
45
73
|
module ErrorInstance
|
74
|
+
include ::Safrano::Contract::Invalid
|
75
|
+
# can(should) be overriden in subclasses
|
76
|
+
def msg
|
77
|
+
@msg
|
78
|
+
end
|
79
|
+
|
46
80
|
def odata_get(req)
|
81
|
+
message = (m = msg.to_s).empty? ? self.class.to_s : m
|
47
82
|
if req.accept?(APPJSON)
|
48
|
-
|
49
|
-
|
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]
|
50
88
|
else
|
51
|
-
[self.class.
|
89
|
+
[self.class.http_code, CT_TEXT, message]
|
52
90
|
end
|
53
91
|
end
|
54
92
|
end
|
55
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
|
+
|
117
|
+
# http Unprocessable Entity for example when trying to
|
118
|
+
# upload duplicated media ressource
|
119
|
+
class UnprocessableEntityError
|
120
|
+
extend ErrorClass
|
121
|
+
include ErrorInstance
|
122
|
+
HTTP_CODE = 422
|
123
|
+
@msg = 'Unprocessable Entity'
|
124
|
+
def initialize(reason)
|
125
|
+
@msg = reason
|
126
|
+
end
|
127
|
+
end
|
128
|
+
|
56
129
|
# http Bad Req.
|
57
130
|
class BadRequestError
|
58
131
|
extend ErrorClass
|
132
|
+
include ErrorInstance
|
59
133
|
HTTP_CODE = 400
|
60
|
-
@msg = 'Bad Request
|
134
|
+
@msg = 'Bad Request'
|
135
|
+
def initialize(reason)
|
136
|
+
@msg = "Bad Request : #{reason}"
|
137
|
+
end
|
138
|
+
end
|
139
|
+
|
140
|
+
# for upload empty media
|
141
|
+
class BadRequestEmptyMediaUpload < BadRequestError
|
142
|
+
include ErrorInstance
|
143
|
+
def initialize(path)
|
144
|
+
@msg = "Bad Request: empty media file #{path}"
|
145
|
+
end
|
61
146
|
end
|
62
147
|
# Generic failed changeset
|
63
148
|
class BadRequestFailedChangeSet < BadRequestError
|
64
|
-
HTTP_CODE = 400
|
65
149
|
@msg = 'Bad Request: Failed changeset '
|
66
150
|
end
|
67
151
|
|
68
152
|
# $value request for a non-media entity
|
69
153
|
class BadRequestNonMediaValue < BadRequestError
|
70
|
-
HTTP_CODE = 400
|
71
154
|
@msg = 'Bad Request: $value request for a non-media entity'
|
72
155
|
end
|
73
156
|
class BadRequestSequelAdapterError < BadRequestError
|
@@ -79,59 +162,127 @@ module OData
|
|
79
162
|
|
80
163
|
# for Syntax error in Filtering
|
81
164
|
class BadRequestFilterParseError < BadRequestError
|
82
|
-
HTTP_CODE = 400
|
83
165
|
@msg = 'Bad Request: Syntax error in $filter'
|
84
166
|
end
|
85
167
|
|
86
|
-
# for
|
87
|
-
class
|
88
|
-
|
89
|
-
|
168
|
+
# for invalid attribute in path
|
169
|
+
class BadRequestInvalidAttribPath < BadRequestError
|
170
|
+
include ErrorInstance
|
171
|
+
def initialize(model, attr)
|
172
|
+
@msg = "Bad Request: the attribute #{attr} is invalid for entityset #{model.entity_set_name}"
|
173
|
+
end
|
174
|
+
end
|
175
|
+
|
176
|
+
# for invalid attribute in $expand param
|
177
|
+
class BadRequestExpandInvalidPath < BadRequestError
|
178
|
+
include ErrorInstance
|
179
|
+
def initialize(model, path)
|
180
|
+
@msg = "Bad Request: the $expand path #{path} is invalid for entityset #{model.entity_set_name}"
|
181
|
+
end
|
182
|
+
end
|
183
|
+
# for invalid properti(es) in $select param
|
184
|
+
class BadRequestSelectInvalidProps < BadRequestError
|
185
|
+
include ErrorInstance
|
186
|
+
def initialize(model, iprops)
|
187
|
+
@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}")
|
188
|
+
end
|
90
189
|
end
|
91
190
|
|
92
191
|
# for Syntax error in $orderby param
|
93
192
|
class BadRequestOrderParseError < BadRequestError
|
94
|
-
HTTP_CODE = 400
|
95
193
|
@msg = 'Bad Request: Syntax error in $orderby'
|
96
194
|
end
|
97
195
|
|
98
196
|
# for $inlinecount error
|
99
197
|
class BadRequestInlineCountParamError < BadRequestError
|
100
|
-
HTTP_CODE = 400
|
101
198
|
@msg = 'Bad Request: wrong $inlinecount parameter'
|
102
199
|
end
|
200
|
+
|
103
201
|
# http not found
|
104
202
|
class ErrorNotFound
|
105
203
|
extend ErrorClass
|
106
204
|
HTTP_CODE = 404
|
107
205
|
@msg = 'The requested ressource was not found'
|
108
206
|
end
|
207
|
+
|
208
|
+
# http not found segment
|
209
|
+
class ErrorNotFoundSegment < ErrorNotFound
|
210
|
+
include ErrorInstance
|
211
|
+
|
212
|
+
def initialize(segment)
|
213
|
+
@msg = "The requested ressource segment #{segment} was not found"
|
214
|
+
end
|
215
|
+
end
|
216
|
+
|
109
217
|
# Transition error (Safrano specific)
|
110
|
-
class ServerTransitionError
|
111
|
-
extend ErrorClass
|
112
|
-
HTTP_CODE = 500
|
218
|
+
class ServerTransitionError < ServerError
|
113
219
|
@msg = 'Server error: Segment could not be parsed'
|
114
220
|
end
|
115
|
-
|
116
|
-
class ServerError
|
117
|
-
extend ErrorClass
|
118
|
-
HTTP_CODE = 500
|
119
|
-
@msg = 'Server error'
|
120
|
-
end
|
221
|
+
|
121
222
|
# not implemented (Safrano specific)
|
122
223
|
class NotImplementedError
|
123
224
|
extend ErrorClass
|
124
225
|
HTTP_CODE = 501
|
125
226
|
end
|
126
|
-
|
127
|
-
|
227
|
+
|
228
|
+
# version not implemented (Safrano specific)
|
229
|
+
class VersionNotImplementedError
|
128
230
|
extend ErrorClass
|
129
231
|
HTTP_CODE = 501
|
232
|
+
@msg = 'The requested OData version is not yet supported'
|
233
|
+
end
|
234
|
+
# batch not implemented (Safrano specific)
|
235
|
+
class BatchNotImplementedError < NotImplementedError
|
130
236
|
@msg = 'Not implemented: OData batch'
|
131
237
|
end
|
238
|
+
|
132
239
|
# error in filter parsing (Safrano specific)
|
133
240
|
class FilterParseError < BadRequestError
|
134
241
|
extend ErrorClass
|
135
|
-
|
242
|
+
end
|
243
|
+
|
244
|
+
class FilterFunctionNotImplementedError < BadRequestError
|
245
|
+
extend ErrorClass
|
246
|
+
include ErrorInstance
|
247
|
+
@msg = 'the requested $filter function is Not implemented'
|
248
|
+
def initialize(xmsg)
|
249
|
+
@msg = xmsg
|
250
|
+
end
|
251
|
+
end
|
252
|
+
class FilterUnknownFunctionError < BadRequestError
|
253
|
+
include ErrorInstance
|
254
|
+
def initialize(badfuncname)
|
255
|
+
@msg = "Bad request: unknown function #{badfuncname} in $filter"
|
256
|
+
end
|
257
|
+
end
|
258
|
+
class FilterParseErrorWrongColumnName < BadRequestError
|
259
|
+
extend ErrorClass
|
260
|
+
@msg = 'Bad request: invalid property name in $filter'
|
261
|
+
end
|
262
|
+
class FilterParseWrappedError < BadRequestError
|
263
|
+
include ErrorInstance
|
264
|
+
def initialize(exception)
|
265
|
+
@msg = exception.to_s
|
266
|
+
end
|
267
|
+
end
|
268
|
+
class ServiceOperationParameterMissing < BadRequestError
|
269
|
+
include ErrorInstance
|
270
|
+
def initialize(missing:, sopname:)
|
271
|
+
@msg = "Bad request: Parameter(s) missing for for service operation #{sopname} : #{missing.join(', ')}"
|
272
|
+
end
|
273
|
+
end
|
274
|
+
|
275
|
+
class ServiceOperationParameterError < BadRequestError
|
276
|
+
include ErrorInstance
|
277
|
+
def initialize(type:, value:, param:, sopname:)
|
278
|
+
@type = type
|
279
|
+
@value = value
|
280
|
+
@param = param
|
281
|
+
@sopname = sopname
|
282
|
+
end
|
283
|
+
|
284
|
+
def msg
|
285
|
+
"Bad request: Parameter #{@param} with value '#{@value}' cannot be converted to type #{@type} for service operation #{@sopname}"
|
286
|
+
end
|
136
287
|
end
|
137
288
|
end
|
data/lib/odata/expand.rb
CHANGED
@@ -1,22 +1,24 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
1
3
|
require 'odata/error.rb'
|
2
4
|
|
3
5
|
# all dataset expanding related classes in our OData module
|
4
6
|
# ie do eager loading
|
5
|
-
module
|
7
|
+
module Safrano
|
6
8
|
# base class for expanding
|
7
9
|
class ExpandBase
|
8
10
|
EmptyExpand = new # re-useable empty expanding (idempotent)
|
9
11
|
EMPTYH = {}.freeze
|
10
12
|
|
11
|
-
def self.factory(expandstr)
|
12
|
-
expandstr.nil? ? EmptyExpand : MultiExpand.new(expandstr)
|
13
|
+
def self.factory(expandstr, model)
|
14
|
+
expandstr.nil? ? EmptyExpand : MultiExpand.new(expandstr, model)
|
13
15
|
end
|
14
16
|
|
15
17
|
# output template
|
16
18
|
attr_reader :template
|
17
19
|
|
18
20
|
def apply_to_dataset(dtcx)
|
19
|
-
dtcx
|
21
|
+
Contract.valid(dtcx)
|
20
22
|
end
|
21
23
|
|
22
24
|
def empty?
|
@@ -24,7 +26,7 @@ module OData
|
|
24
26
|
end
|
25
27
|
|
26
28
|
def parse_error?
|
27
|
-
|
29
|
+
Contract::OK
|
28
30
|
end
|
29
31
|
|
30
32
|
def template
|
@@ -60,7 +62,7 @@ module OData
|
|
60
62
|
end
|
61
63
|
|
62
64
|
def apply_to_dataset(dtcx)
|
63
|
-
dtcx
|
65
|
+
Contract.valid(dtcx)
|
64
66
|
end
|
65
67
|
|
66
68
|
def build_arg
|
@@ -70,11 +72,6 @@ module OData
|
|
70
72
|
@template = DEEPH_1.call(@nodes)
|
71
73
|
end
|
72
74
|
|
73
|
-
def parse_error?
|
74
|
-
# todo
|
75
|
-
false
|
76
|
-
end
|
77
|
-
|
78
75
|
def empty?
|
79
76
|
false
|
80
77
|
end
|
@@ -85,19 +82,22 @@ module OData
|
|
85
82
|
COMASPLIT = /\s*,\s*/.freeze
|
86
83
|
attr_reader :template
|
87
84
|
|
88
|
-
|
85
|
+
# Note: if you change this method, please also update arity_full_monkey?
|
86
|
+
# see below
|
87
|
+
def initialize(expandstr, model)
|
89
88
|
expandstr.strip!
|
89
|
+
@model = model
|
90
90
|
@expandp = expandstr
|
91
|
-
@exlist = []
|
92
91
|
|
93
|
-
@
|
92
|
+
@exstrlist = expandstr.split(COMASPLIT)
|
93
|
+
@exlist = @exstrlist.map { |exstr| Expand.new(exstr) }
|
94
94
|
build_template
|
95
95
|
end
|
96
96
|
|
97
97
|
def apply_to_dataset(dtcx)
|
98
98
|
# use eager loading for each used association
|
99
99
|
@exlist.each { |exp| dtcx = dtcx.eager(exp.arg) }
|
100
|
-
dtcx
|
100
|
+
Contract.valid(dtcx)
|
101
101
|
end
|
102
102
|
|
103
103
|
def build_template
|
@@ -112,8 +112,11 @@ module OData
|
|
112
112
|
end
|
113
113
|
|
114
114
|
def parse_error?
|
115
|
-
|
116
|
-
|
115
|
+
@exstrlist.each do |expstr|
|
116
|
+
return BadRequestExpandInvalidPath.new(@model, expstr) unless @model.expand_path_valid? expstr
|
117
|
+
end
|
118
|
+
|
119
|
+
Contract::OK
|
117
120
|
end
|
118
121
|
|
119
122
|
def empty?
|
@@ -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
|