safrano 0.4.4 → 0.5.2
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/lib/odata/attribute.rb +2 -2
- data/lib/odata/collection.rb +15 -10
- data/lib/odata/collection_filter.rb +5 -5
- data/lib/odata/collection_media.rb +63 -25
- data/lib/odata/complex_type.rb +185 -43
- data/lib/odata/entity.rb +31 -8
- data/lib/odata/error.rb +28 -0
- data/lib/odata/filter/base.rb +5 -0
- data/lib/odata/filter/parse.rb +6 -0
- data/lib/odata/filter/sequel.rb +15 -0
- data/lib/odata/filter/token.rb +13 -10
- data/lib/odata/filter/tree.rb +4 -4
- data/lib/odata/function_import.rb +35 -34
- data/lib/odata/model_ext.rb +28 -5
- data/lib/odata/transition.rb +22 -2
- data/lib/odata/walker.rb +23 -5
- data/lib/safrano/contract.rb +7 -9
- data/lib/safrano/core.rb +12 -12
- data/lib/safrano/deprecation.rb +5 -5
- data/lib/safrano/multipart.rb +4 -4
- data/lib/safrano/rack_app.rb +1 -1
- data/lib/safrano/request.rb +3 -2
- data/lib/safrano/service.rb +38 -21
- data/lib/safrano/version.rb +1 -1
- metadata +9 -9
data/lib/odata/entity.rb
CHANGED
@@ -112,10 +112,11 @@ module Safrano
|
|
112
112
|
selvals
|
113
113
|
end
|
114
114
|
|
115
|
-
# post
|
116
|
-
|
117
|
-
|
118
|
-
|
115
|
+
# some clients wrongly expect post payload with the new entity in an array
|
116
|
+
# TODO quirks array mode !
|
117
|
+
def to_odata_array_json(request:)
|
118
|
+
innerj = request.service.get_coll_odata_h(array: [self],
|
119
|
+
template: self.class.default_template).to_json
|
119
120
|
"#{DJ_OPEN}#{innerj}#{DJ_CLOSE}"
|
120
121
|
end
|
121
122
|
|
@@ -124,7 +125,7 @@ module Safrano
|
|
124
125
|
end
|
125
126
|
|
126
127
|
def copy_request_infos(req)
|
127
|
-
@params = req.params
|
128
|
+
@params = @inactive_query_params ? EMPTY_HASH : req.params
|
128
129
|
@do_links = req.walker.do_links
|
129
130
|
@uparms = UrlParameters4Single.new(self, @params)
|
130
131
|
end
|
@@ -150,7 +151,11 @@ module Safrano
|
|
150
151
|
@uparms.check_all.tap_valid { return odata_get_output(req) }
|
151
152
|
.tap_error { |e| return e.odata_get(req) }
|
152
153
|
end
|
153
|
-
|
154
|
+
def inactive_query_params
|
155
|
+
@inactive_query_params = true
|
156
|
+
self # chaining
|
157
|
+
end
|
158
|
+
|
154
159
|
DELETE_REL_AND_ENTY = lambda do |entity, assoc, parent|
|
155
160
|
Safrano.remove_nav_relation(assoc, parent)
|
156
161
|
entity.destroy(transaction: false)
|
@@ -263,6 +268,7 @@ module Safrano
|
|
263
268
|
ret
|
264
269
|
end
|
265
270
|
end
|
271
|
+
|
266
272
|
# end of module SafranoEntity
|
267
273
|
module Entity
|
268
274
|
include EntityBase
|
@@ -438,5 +444,22 @@ module Safrano
|
|
438
444
|
pk_hash.values
|
439
445
|
end
|
440
446
|
end
|
441
|
-
|
442
|
-
|
447
|
+
|
448
|
+
module EntityCreateStandardOutput
|
449
|
+
# Json formatter for a create entity POST call / Standard version; return as json object
|
450
|
+
def to_odata_create_json(request:)
|
451
|
+
# TODO Perf: reduce method call overhead
|
452
|
+
# we added this redirection for readability and flexibility
|
453
|
+
to_odata_json(request: request)
|
454
|
+
end
|
455
|
+
end
|
456
|
+
|
457
|
+
module EntityCreateArrayOutput
|
458
|
+
# Json formatter for a create entity POST call Array version
|
459
|
+
def to_odata_create_json(request:)
|
460
|
+
# TODO Perf: reduce method call overhead
|
461
|
+
# we added this redirection for readability and flexibility
|
462
|
+
to_odata_array_json(request: request)
|
463
|
+
end
|
464
|
+
end
|
465
|
+
end # end of Module OData
|
data/lib/odata/error.rb
CHANGED
@@ -37,6 +37,15 @@ module Safrano
|
|
37
37
|
super(msg, symbname)
|
38
38
|
end
|
39
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
|
40
49
|
end
|
41
50
|
|
42
51
|
# base module for HTTP errors, when used as a Error Class
|
@@ -105,6 +114,18 @@ module Safrano
|
|
105
114
|
end
|
106
115
|
end
|
107
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
|
+
|
108
129
|
# http Bad Req.
|
109
130
|
class BadRequestError
|
110
131
|
extend ErrorClass
|
@@ -116,6 +137,13 @@ module Safrano
|
|
116
137
|
end
|
117
138
|
end
|
118
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
|
146
|
+
end
|
119
147
|
# Generic failed changeset
|
120
148
|
class BadRequestFailedChangeSet < BadRequestError
|
121
149
|
@msg = 'Bad Request: Failed changeset '
|
data/lib/odata/filter/base.rb
CHANGED
@@ -57,6 +57,11 @@ module Safrano
|
|
57
57
|
class Literal < Leave
|
58
58
|
end
|
59
59
|
|
60
|
+
# Null Literal is unquoted null word
|
61
|
+
class NullLiteral < Literal
|
62
|
+
LEUQES = nil
|
63
|
+
end
|
64
|
+
|
60
65
|
# Qualit (qualified lits) are words separated by /
|
61
66
|
# path/path/path/attrib
|
62
67
|
class Qualit < Literal
|
data/lib/odata/filter/parse.rb
CHANGED
@@ -151,6 +151,12 @@ module Safrano
|
|
151
151
|
grow_at_cursor(Literal.new(tok))
|
152
152
|
end
|
153
153
|
|
154
|
+
when :NullLiteral
|
155
|
+
with_accepted(tok, typ) do
|
156
|
+
@cursor.update_state(tok, typ)
|
157
|
+
grow_at_cursor(NullLiteral.new(tok))
|
158
|
+
end
|
159
|
+
|
154
160
|
when :Qualit
|
155
161
|
with_accepted(tok, typ) do
|
156
162
|
@cursor.update_state(tok, typ)
|
data/lib/odata/filter/sequel.rb
CHANGED
@@ -183,6 +183,13 @@ module Safrano
|
|
183
183
|
end
|
184
184
|
Contract.collect_result!(@children[0].leuqes(jh),
|
185
185
|
@children[1].leuqes(jh)) do |c0, c1|
|
186
|
+
if c1 == NullLiteral::LEUQES
|
187
|
+
if @value == :eq
|
188
|
+
leuqes_op = :IS
|
189
|
+
elsif @value == :ne
|
190
|
+
leuqes_op = :'IS NOT'
|
191
|
+
end
|
192
|
+
end
|
186
193
|
Sequel::SQL::BooleanExpression.new(leuqes_op, c0, c1)
|
187
194
|
end
|
188
195
|
end
|
@@ -262,6 +269,14 @@ module Safrano
|
|
262
269
|
end
|
263
270
|
end
|
264
271
|
|
272
|
+
# Null
|
273
|
+
class NullLiteral
|
274
|
+
def leuqes(jh)
|
275
|
+
# Sequel's representation of NULL
|
276
|
+
success LEUQES
|
277
|
+
end
|
278
|
+
end
|
279
|
+
|
265
280
|
# Qualit (qualified lits) are words separated by /
|
266
281
|
class Qualit
|
267
282
|
def leuqes(jh)
|
data/lib/odata/filter/token.rb
CHANGED
@@ -10,13 +10,14 @@ module Safrano
|
|
10
10
|
day hour minute month second year
|
11
11
|
round floor ceiling].freeze
|
12
12
|
FUNCRGX = FUNCNAMES.join('|')
|
13
|
+
NULLRGX = 'null|NULL|Null'
|
13
14
|
QSTRINGRGX = /'((?:[^']|(?:'{2}))*)'/.freeze
|
14
15
|
BINOBOOL = '[eE][qQ]|[LlgGNn][eETt]|[aA][nN][dD]|[oO][rR]'
|
15
16
|
BINOARITHM = '[aA][dD][dD]|[sS][uU][bB]|[mM][uU][lL]|[dD][iI][vV]|[mM][oO][dD]'
|
16
|
-
NOTRGX = 'not|NOT'
|
17
|
+
NOTRGX = 'not|NOT|Not'
|
17
18
|
FPRGX = '\d+(?:\.\d+)?(?:e[+-]?\d+)?'
|
18
19
|
QUALITRGX = '\w+(?:\/\w+)+'
|
19
|
-
RGX = /(#{FUNCRGX})|([\(\),])|(#{BINOBOOL})\s+|(#{BINOARITHM})|(#{NOTRGX})|#{QSTRINGRGX}|(#{FPRGX})|(#{QUALITRGX})|(\w+)|(')/.freeze
|
20
|
+
RGX = /(#{FUNCRGX})|(#{NULLRGX})|([\(\),])|(#{BINOBOOL})\s+|(#{BINOARITHM})|(#{NOTRGX})|#{QSTRINGRGX}|(#{FPRGX})|(#{QUALITRGX})|(\w+)|(')/.freeze
|
20
21
|
|
21
22
|
def each_typed_token(inp)
|
22
23
|
typ = nil
|
@@ -34,27 +35,29 @@ module Safrano
|
|
34
35
|
when 0
|
35
36
|
:FuncTree
|
36
37
|
when 1
|
38
|
+
:NullLiteral
|
39
|
+
when 2
|
37
40
|
case found
|
38
41
|
when '(', ')'
|
39
42
|
:Delimiter
|
40
43
|
when ','
|
41
44
|
:Separator
|
42
45
|
end
|
43
|
-
when 2
|
44
|
-
:BinopBool
|
45
46
|
when 3
|
46
|
-
:
|
47
|
+
:BinopBool
|
47
48
|
when 4
|
48
|
-
:
|
49
|
+
:BinopArithm
|
49
50
|
when 5
|
50
|
-
:
|
51
|
+
:UnopTree
|
51
52
|
when 6
|
52
|
-
:
|
53
|
+
:QString
|
53
54
|
when 7
|
54
|
-
:
|
55
|
+
:FPNumber
|
55
56
|
when 8
|
56
|
-
:
|
57
|
+
:Qualit
|
57
58
|
when 9
|
59
|
+
:Literal
|
60
|
+
when 10
|
58
61
|
:unmatchedQuote
|
59
62
|
end
|
60
63
|
yield found, typ
|
data/lib/odata/filter/tree.rb
CHANGED
@@ -65,7 +65,7 @@ module Safrano
|
|
65
65
|
# nil is considered as accepted, otherwise non-nil=the error
|
66
66
|
def accept?(tok, typ)
|
67
67
|
case typ
|
68
|
-
when :Literal, :Qualit, :QString, :FuncTree, :ArgTree,
|
68
|
+
when :Literal, :NullLiteral, :Qualit, :QString, :FuncTree, :ArgTree,
|
69
69
|
:UnopTree, :FPNumber
|
70
70
|
nil
|
71
71
|
when :Delimiter
|
@@ -232,7 +232,7 @@ module Safrano
|
|
232
232
|
|
233
233
|
def update_state(_tok, typ)
|
234
234
|
case typ
|
235
|
-
when :Literal, :Qualit, :QString, :FuncTree, :BinopBool, :BinopArithm, :UnopTree, :FPNumber
|
235
|
+
when :Literal, :NullLiteral, :Qualit, :QString, :FuncTree, :BinopBool, :BinopArithm, :UnopTree, :FPNumber
|
236
236
|
@state = :closed
|
237
237
|
end
|
238
238
|
end
|
@@ -297,7 +297,7 @@ module Safrano
|
|
297
297
|
@state = :closed
|
298
298
|
when :Separator
|
299
299
|
@state = :sep
|
300
|
-
when :Literal, :Qualit, :QString, :FuncTree, :FPNumber
|
300
|
+
when :Literal, :NullLiteral, :Qualit, :QString, :FuncTree, :FPNumber
|
301
301
|
@state = :val
|
302
302
|
end
|
303
303
|
end
|
@@ -327,7 +327,7 @@ module Safrano
|
|
327
327
|
elsif @state == :sep
|
328
328
|
Parser::ErrorInvalidToken.new(tok, typ, self)
|
329
329
|
end
|
330
|
-
when :Literal, :Qualit, :QString, :FuncTree, :FPNumber
|
330
|
+
when :Literal, :NullLiteral, :Qualit, :QString, :FuncTree, :FPNumber
|
331
331
|
if (@state == :open) || (@state == :sep)
|
332
332
|
if @parent.arity_full?(@children.size)
|
333
333
|
Parser::ErrorInvalidArity.new(tok, typ, self)
|
@@ -2,8 +2,10 @@
|
|
2
2
|
|
3
3
|
require_relative 'complex_type'
|
4
4
|
require_relative 'edm/primitive_types'
|
5
|
+
require_relative 'transition'
|
6
|
+
|
5
7
|
module Safrano
|
6
|
-
def
|
8
|
+
def self.FunctionImport(name)
|
7
9
|
FunctionImport::Function.new(name)
|
8
10
|
end
|
9
11
|
|
@@ -19,12 +21,12 @@ module Safrano
|
|
19
21
|
end
|
20
22
|
|
21
23
|
def allowed_transitions
|
22
|
-
[Safrano::
|
24
|
+
[Safrano::TransitionExecuteFunc]
|
23
25
|
end
|
24
26
|
|
25
27
|
def input(**parmtypes)
|
26
28
|
@input = {}
|
27
|
-
parmtypes.each
|
29
|
+
parmtypes.each do |k, t|
|
28
30
|
@input[k] = case t.name
|
29
31
|
when 'Integer'
|
30
32
|
Safrano::Edm::Edm::Int32
|
@@ -37,10 +39,16 @@ module Safrano
|
|
37
39
|
else
|
38
40
|
t
|
39
41
|
end
|
40
|
-
|
42
|
+
end
|
41
43
|
self
|
42
44
|
end
|
43
|
-
|
45
|
+
|
46
|
+
def auto_query_parameters
|
47
|
+
@auto_query_params = true
|
48
|
+
self # chaining
|
49
|
+
end
|
50
|
+
alias auto_query_params auto_query_parameters
|
51
|
+
|
44
52
|
def return(klassmod, &proc)
|
45
53
|
raise('Please provide a code block') unless block_given?
|
46
54
|
|
@@ -49,7 +57,7 @@ module Safrano
|
|
49
57
|
else
|
50
58
|
# if it's neither a ComplexType nor a Model-Entity
|
51
59
|
# --> assume it is a Primitive
|
52
|
-
|
60
|
+
ResultDefinition.asPrimitiveType(klassmod)
|
53
61
|
end
|
54
62
|
@proc = proc
|
55
63
|
self
|
@@ -63,7 +71,8 @@ module Safrano
|
|
63
71
|
else
|
64
72
|
# if it's neither a ComplexType nor a Modle-Entity
|
65
73
|
# --> assume it is a Primitive
|
66
|
-
ResultAsPrimitiveTypeColl.new(klassmod)
|
74
|
+
# ResultAsPrimitiveTypeColl.new(klassmod)
|
75
|
+
ResultDefinition.asPrimitiveTypeColl(klassmod)
|
67
76
|
end
|
68
77
|
@proc = proc
|
69
78
|
self
|
@@ -91,20 +100,20 @@ module Safrano
|
|
91
100
|
return nil unless @input # anything to check ?
|
92
101
|
|
93
102
|
# do we have all parameters provided ?
|
94
|
-
check_missing_params.tap_error
|
103
|
+
check_missing_params.tap_error { |error| return error }
|
95
104
|
# ==> all params were provided
|
96
105
|
|
97
106
|
# now we shall check the content and type of the parameters
|
98
|
-
@input.each
|
107
|
+
@input.each do |ksym, typ|
|
99
108
|
typ.convert_from_urlparam(v = @params[ksym.to_s])
|
100
109
|
.tap_valid do |retval|
|
101
110
|
@funcparams[ksym] = retval
|
102
111
|
end
|
103
|
-
.tap_error do
|
112
|
+
.tap_error do
|
104
113
|
# return is really needed here, or we end up returning nil below
|
105
114
|
return parameter_convertion_error(ksym, typ, v)
|
106
115
|
end
|
107
|
-
|
116
|
+
end
|
108
117
|
nil
|
109
118
|
end
|
110
119
|
|
@@ -125,41 +134,33 @@ module Safrano
|
|
125
134
|
# EntitySet= @entity_set ,
|
126
135
|
'ReturnType' => @returning.type_metadata,
|
127
136
|
'm:HttpMethod' => @http_method)
|
128
|
-
@input.each
|
137
|
+
@input.each do |iname, type|
|
129
138
|
funky.add_element('Parameter',
|
130
139
|
'Name' => iname.to_s,
|
131
140
|
'Type' => type.type_name,
|
132
141
|
'Mode' => 'In')
|
133
|
-
|
142
|
+
end if @input
|
134
143
|
funky
|
135
144
|
end
|
136
|
-
|
137
|
-
def
|
145
|
+
|
146
|
+
def with_transition_validated(req)
|
138
147
|
# initialize_params
|
148
|
+
@params = req.params
|
139
149
|
return yield unless (@error = check_url_func_params)
|
140
150
|
|
141
|
-
@error
|
151
|
+
[nil, :error, @error] if @error
|
142
152
|
end
|
143
|
-
|
144
|
-
|
145
|
-
|
146
|
-
|
147
|
-
|
148
|
-
|
149
|
-
def odata_get_output(req)
|
150
|
-
[200, EMPTY_HASH, [to_odata_json(req)]]
|
151
|
-
end
|
152
|
-
|
153
|
-
def odata_get(req)
|
154
|
-
@params = req.params
|
155
|
-
|
156
|
-
with_validated_get(req) do
|
157
|
-
odata_get_output(req)
|
153
|
+
|
154
|
+
|
155
|
+
def do_execute_func(req)
|
156
|
+
with_transition_validated(req) do
|
157
|
+
result = @proc.call(**@funcparams)
|
158
|
+
[@returning.do_execute_func_result(result, req, @auto_query_params), :run]
|
158
159
|
end
|
159
160
|
end
|
160
|
-
|
161
|
-
def
|
162
|
-
[
|
161
|
+
|
162
|
+
def transition_execute_func(_match_result)
|
163
|
+
[self, :run_with_execute_func]
|
163
164
|
end
|
164
165
|
end
|
165
166
|
end
|
data/lib/odata/model_ext.rb
CHANGED
@@ -81,11 +81,11 @@ module Safrano
|
|
81
81
|
end
|
82
82
|
|
83
83
|
def return_as_collection_descriptor
|
84
|
-
Safrano::FunctionImport::
|
84
|
+
Safrano::FunctionImport::ResultDefinition.asEntityColl(self)
|
85
85
|
end
|
86
86
|
|
87
87
|
def return_as_instance_descriptor
|
88
|
-
Safrano::FunctionImport::
|
88
|
+
Safrano::FunctionImport::ResultDefinition.asEntity(self)
|
89
89
|
end
|
90
90
|
|
91
91
|
def execute_deferred_iblock
|
@@ -94,8 +94,10 @@ module Safrano
|
|
94
94
|
|
95
95
|
# Factory json-> Model Object instance
|
96
96
|
def new_from_hson_h(hash)
|
97
|
-
enty = new
|
98
|
-
enty.set_fields(hash, data_fields, missing: :skip)
|
97
|
+
#enty = new
|
98
|
+
#enty.set_fields(hash, data_fields, missing: :skip)
|
99
|
+
enty = create(hash)
|
100
|
+
#enty.set(hash)
|
99
101
|
enty
|
100
102
|
end
|
101
103
|
|
@@ -299,6 +301,14 @@ module Safrano
|
|
299
301
|
|
300
302
|
attr_class = assoc[:class_name].constantize
|
301
303
|
lattr_name_str = (attr_name_str || assoc_symb.to_s)
|
304
|
+
|
305
|
+
# check duplicate attributes names
|
306
|
+
raise Safrano::API::ModelDuplicateAttributeError.new(self, lattr_name_str) if @columns.include? lattr_name_str.to_sym
|
307
|
+
|
308
|
+
if @nav_entity_attribs_keys
|
309
|
+
raise Safrano::API::ModelDuplicateAttributeError.new(self, lattr_name_str) if @nav_entity_attribs_keys.include? lattr_name_str
|
310
|
+
end
|
311
|
+
|
302
312
|
@nav_collection_attribs[lattr_name_str] = attr_class
|
303
313
|
@nav_collection_attribs_keys << lattr_name_str
|
304
314
|
@nav_collection_url_regexp = @nav_collection_attribs_keys.join('|')
|
@@ -317,6 +327,14 @@ module Safrano
|
|
317
327
|
|
318
328
|
attr_class = assoc[:class_name].constantize
|
319
329
|
lattr_name_str = (attr_name_str || assoc_symb.to_s)
|
330
|
+
|
331
|
+
# check duplicate attributes names
|
332
|
+
raise Safrano::API::ModelDuplicateAttributeError.new(self, lattr_name_str) if @columns.include? lattr_name_str.to_sym
|
333
|
+
|
334
|
+
if @nav_collection_attribs_keys
|
335
|
+
raise Safrano::API::ModelDuplicateAttributeError.new(self, lattr_name_str) if @nav_collection_attribs_keys.include? lattr_name_str
|
336
|
+
end
|
337
|
+
|
320
338
|
@nav_entity_attribs[lattr_name_str] = attr_class
|
321
339
|
@nav_entity_attribs_keys << lattr_name_str
|
322
340
|
@nav_entity_url_regexp = @nav_entity_attribs_keys.join('|')
|
@@ -352,6 +370,9 @@ module Safrano
|
|
352
370
|
|
353
371
|
build_allowed_transitions
|
354
372
|
build_entity_allowed_transitions
|
373
|
+
|
374
|
+
# for media
|
375
|
+
finalize_media if self.respond_to? :finalize_media
|
355
376
|
end
|
356
377
|
|
357
378
|
KEYPRED_URL_REGEXP = /\A\(\s*'?([\w=,'\s]+)'?\s*\)(.*)/.freeze
|
@@ -608,7 +629,9 @@ module Safrano
|
|
608
629
|
req.register_content_id_ref(new_entity)
|
609
630
|
new_entity.copy_request_infos(req)
|
610
631
|
# json is default content type so we dont need to specify it here again
|
611
|
-
|
632
|
+
# TODO quirks array mode !
|
633
|
+
# [201, EMPTY_HASH, new_entity.to_odata_post_json(service: req.service)]
|
634
|
+
[201, EMPTY_HASH, new_entity.to_odata_create_json(request: req)]
|
612
635
|
else # TODO: other formats
|
613
636
|
415
|
614
637
|
end
|