safrano 0.3.3 → 0.4.3
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 +9 -8
- data/lib/odata/batch.rb +8 -8
- data/lib/odata/collection.rb +239 -92
- data/lib/odata/collection_filter.rb +40 -9
- data/lib/odata/collection_media.rb +159 -28
- data/lib/odata/collection_order.rb +46 -36
- data/lib/odata/common_logger.rb +37 -12
- data/lib/odata/entity.rb +188 -99
- data/lib/odata/error.rb +60 -12
- data/lib/odata/expand.rb +123 -0
- data/lib/odata/filter/base.rb +66 -0
- data/lib/odata/filter/error.rb +33 -0
- data/lib/odata/filter/parse.rb +6 -12
- data/lib/odata/filter/sequel.rb +42 -29
- data/lib/odata/filter/sequel_function_adapter.rb +147 -0
- data/lib/odata/filter/token.rb +5 -1
- data/lib/odata/filter/tree.rb +45 -29
- data/lib/odata/navigation_attribute.rb +60 -27
- data/lib/odata/relations.rb +2 -2
- data/lib/odata/select.rb +42 -0
- data/lib/odata/url_parameters.rb +51 -36
- data/lib/odata/walker.rb +6 -6
- data/lib/safrano.rb +23 -13
- data/lib/{safrano_core.rb → safrano/core.rb} +12 -4
- data/lib/{multipart.rb → safrano/multipart.rb} +17 -26
- data/lib/{odata_rack_builder.rb → safrano/odata_rack_builder.rb} +0 -1
- data/lib/{rack_app.rb → safrano/rack_app.rb} +12 -10
- data/lib/{request.rb → safrano/request.rb} +8 -14
- data/lib/{response.rb → safrano/response.rb} +1 -2
- data/lib/{sequel_join_by_paths.rb → safrano/sequel_join_by_paths.rb} +1 -1
- data/lib/{service.rb → safrano/service.rb} +162 -131
- data/lib/safrano/version.rb +3 -0
- data/lib/sequel/plugins/join_by_paths.rb +11 -10
- metadata +33 -16
- data/lib/version.rb +0 -4
@@ -0,0 +1,147 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require_relative './tree.rb'
|
4
|
+
require_relative './sequel.rb'
|
5
|
+
|
6
|
+
module OData
|
7
|
+
module Filter
|
8
|
+
# sqlite adapter specific function handler
|
9
|
+
module FuncTreeSqlite
|
10
|
+
def substringof_sig2(jh)
|
11
|
+
# substringof(name, '__Route du Rhum__') -->
|
12
|
+
# '__Route du Rhum__' contains name as a substring
|
13
|
+
# sqlite uses instr()
|
14
|
+
|
15
|
+
substr_func = Sequel.function(:instr, args[1].leuqes(jh), args[0].leuqes(jh))
|
16
|
+
|
17
|
+
Sequel::SQL::BooleanExpression.new(:>, substr_func, 0)
|
18
|
+
end
|
19
|
+
# %d day of month: 00
|
20
|
+
# %f fractional seconds: SS.SSS
|
21
|
+
# %H hour: 00-24
|
22
|
+
# %j day of year: 001-366
|
23
|
+
# %J Julian day number
|
24
|
+
# %m month: 01-12
|
25
|
+
# %M minute: 00-59
|
26
|
+
# %s seconds since 1970-01-01
|
27
|
+
# %S seconds: 00-59
|
28
|
+
# %w day of week 0-6 with Sunday==0
|
29
|
+
# %W week of year: 00-53
|
30
|
+
# %Y year: 0000-9999
|
31
|
+
# %% %
|
32
|
+
|
33
|
+
# sqlite does not have extract but recommends to use strftime
|
34
|
+
def year(jh)
|
35
|
+
Sequel.function(:strftime, '%Y', args.first.leuqes(jh)).cast(:integer)
|
36
|
+
end
|
37
|
+
|
38
|
+
def month(jh)
|
39
|
+
Sequel.function(:strftime, '%m', args.first.leuqes(jh)).cast(:integer)
|
40
|
+
end
|
41
|
+
|
42
|
+
def second(jh)
|
43
|
+
Sequel.function(:strftime, '%S', args.first.leuqes(jh)).cast(:integer)
|
44
|
+
end
|
45
|
+
|
46
|
+
def minute(jh)
|
47
|
+
Sequel.function(:strftime, '%M', args.first.leuqes(jh)).cast(:integer)
|
48
|
+
end
|
49
|
+
|
50
|
+
def hour(jh)
|
51
|
+
Sequel.function(:strftime, '%H', args.first.leuqes(jh)).cast(:integer)
|
52
|
+
end
|
53
|
+
|
54
|
+
def day(jh)
|
55
|
+
Sequel.function(:strftime, '%d', args.first.leuqes(jh)).cast(:integer)
|
56
|
+
end
|
57
|
+
|
58
|
+
def floor(jh)
|
59
|
+
raise OData::Filter::FunctionNotImplemented, "$filter function 'floor' is not implemented in sqlite adapter"
|
60
|
+
end
|
61
|
+
|
62
|
+
def ceiling(jh)
|
63
|
+
raise OData::Filter::FunctionNotImplemented, "$filter function 'ceiling' is not implemented in sqlite adapter"
|
64
|
+
end
|
65
|
+
end
|
66
|
+
# re-useable module with math floor/ceil functions for those adapters having these SQL funcs
|
67
|
+
module MathFloorCeilFuncTree
|
68
|
+
def floor(jh)
|
69
|
+
Sequel.function(:floor, args.first.leuqes(jh))
|
70
|
+
end
|
71
|
+
|
72
|
+
def ceiling(jh)
|
73
|
+
Sequel.function(:ceil, args.first.leuqes(jh))
|
74
|
+
end
|
75
|
+
end
|
76
|
+
|
77
|
+
# re-useable module with Datetime functions with extract()
|
78
|
+
module DateTimeFuncTreeExtract
|
79
|
+
def year(jh)
|
80
|
+
args.first.leuqes(jh).extract(:year)
|
81
|
+
end
|
82
|
+
|
83
|
+
def year(jh)
|
84
|
+
args.first.leuqes(jh).extract(:year)
|
85
|
+
end
|
86
|
+
|
87
|
+
def month(jh)
|
88
|
+
args.first.leuqes(jh).extract(:month)
|
89
|
+
end
|
90
|
+
|
91
|
+
def second(jh)
|
92
|
+
args.first.leuqes(jh).extract(:second)
|
93
|
+
end
|
94
|
+
|
95
|
+
def minute(jh)
|
96
|
+
args.first.leuqes(jh).extract(:minute)
|
97
|
+
end
|
98
|
+
|
99
|
+
def hour(jh)
|
100
|
+
args.first.leuqes(jh).extract(:hour)
|
101
|
+
end
|
102
|
+
|
103
|
+
def day(jh)
|
104
|
+
args.first.leuqes(jh).extract(:day)
|
105
|
+
end
|
106
|
+
end
|
107
|
+
|
108
|
+
# postgresql adapter specific function handler
|
109
|
+
module FuncTreePostgres
|
110
|
+
def substringof_sig2(jh)
|
111
|
+
# substringof(name, '__Route du Rhum__') -->
|
112
|
+
# '__Route du Rhum__' contains name as a substring
|
113
|
+
# postgres does not know instr() but has strpos
|
114
|
+
substr_func = Sequel.function(:strpos, args[1].leuqes(jh), args[0].leuqes(jh))
|
115
|
+
|
116
|
+
Sequel::SQL::BooleanExpression.new(:>, substr_func, 0)
|
117
|
+
end
|
118
|
+
|
119
|
+
# postgres uses extract()
|
120
|
+
include DateTimeFuncTreeExtract
|
121
|
+
|
122
|
+
# postgres has floor/ceil funcs
|
123
|
+
include MathFloorCeilFuncTree
|
124
|
+
end
|
125
|
+
|
126
|
+
# default adapter function handler for all others... try to use the most common version
|
127
|
+
# :substring --> instr because here is seems Postgres is special
|
128
|
+
# datetime funcs --> exctract, here sqlite is special(uses format)
|
129
|
+
# note: we dont test this, provided as an example/template, might work eg for mysql
|
130
|
+
module FuncTreeDefault
|
131
|
+
def substringof_sig2(jh)
|
132
|
+
# substringof(name, '__Route du Rhum__') -->
|
133
|
+
# '__Route du Rhum__' contains name as a substring
|
134
|
+
# instr() seems to be the most common substring func
|
135
|
+
substr_func = Sequel.function(:instr, args[1].leuqes(jh), args[0].leuqes(jh))
|
136
|
+
|
137
|
+
Sequel::SQL::BooleanExpression.new(:>, substr_func, 0)
|
138
|
+
end
|
139
|
+
|
140
|
+
# XYZ uses extract() ?
|
141
|
+
include DateTimeFuncTreeExtract
|
142
|
+
|
143
|
+
# ... assume SQL
|
144
|
+
include MathFloorCeilFuncTree
|
145
|
+
end
|
146
|
+
end
|
147
|
+
end
|
data/lib/odata/filter/token.rb
CHANGED
@@ -1,3 +1,5 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
1
3
|
# top level OData namespace
|
2
4
|
module OData
|
3
5
|
module Filter
|
@@ -5,7 +7,9 @@ module OData
|
|
5
7
|
# Input tokenizer
|
6
8
|
module Token
|
7
9
|
FUNCNAMES = %w[concat substringof endswith startswith length indexof
|
8
|
-
replace substring trim toupper tolower
|
10
|
+
replace substring trim toupper tolower
|
11
|
+
day hour minute month second year
|
12
|
+
round floor ceiling].freeze
|
9
13
|
FUNCRGX = FUNCNAMES.join('|').freeze
|
10
14
|
QSTRINGRGX = /'((?:[^']|(?:\'{2}))*)'/.freeze
|
11
15
|
BINOBOOL = '[eE][qQ]|[LlgGNn][eETt]|[aA][nN][dD]|[oO][rR]'.freeze
|
data/lib/odata/filter/tree.rb
CHANGED
@@ -1,3 +1,6 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require_relative './base.rb'
|
1
4
|
require_relative './error.rb'
|
2
5
|
|
3
6
|
module OData
|
@@ -16,17 +19,22 @@ module OData
|
|
16
19
|
end
|
17
20
|
|
18
21
|
# Leaves are Nodes with a parent but no children
|
19
|
-
class Leave
|
22
|
+
class Leave
|
20
23
|
attr_accessor :parent
|
21
24
|
def accept?(tok, typ)
|
22
25
|
[false, Parser::ErrorInvalidToken(tok, typ)]
|
23
26
|
end
|
24
27
|
|
25
28
|
def check_types; end
|
29
|
+
|
30
|
+
def attach(child)
|
31
|
+
# TODO better reporting of error infos
|
32
|
+
raise ErrorLeaveChild
|
33
|
+
end
|
26
34
|
end
|
27
35
|
|
28
36
|
# RootTrees have childrens but no parent
|
29
|
-
class RootTree
|
37
|
+
class RootTree
|
30
38
|
attr_reader :children
|
31
39
|
attr_accessor :state
|
32
40
|
def initialize(val: :root, &block)
|
@@ -71,7 +79,7 @@ module OData
|
|
71
79
|
end
|
72
80
|
|
73
81
|
# Tree's have Parent and children
|
74
|
-
class Tree
|
82
|
+
class Tree
|
75
83
|
attr_accessor :parent
|
76
84
|
|
77
85
|
def initialize(val)
|
@@ -80,7 +88,7 @@ module OData
|
|
80
88
|
end
|
81
89
|
|
82
90
|
# For functions... should have a single child---> the argument list
|
83
|
-
class FuncTree
|
91
|
+
class FuncTree
|
84
92
|
def initialize(val)
|
85
93
|
super(val.downcase.to_sym)
|
86
94
|
end
|
@@ -143,7 +151,7 @@ module OData
|
|
143
151
|
# Indentity Func to use as "parent" func of parenthesis expressions
|
144
152
|
# --> allow to handle generically parenthesis always as argument of
|
145
153
|
# some function
|
146
|
-
class IdentityFuncTree
|
154
|
+
class IdentityFuncTree
|
147
155
|
def initialize
|
148
156
|
super(:__indentity)
|
149
157
|
end
|
@@ -160,7 +168,7 @@ module OData
|
|
160
168
|
end
|
161
169
|
|
162
170
|
# unary op eg. NOT
|
163
|
-
class UnopTree
|
171
|
+
class UnopTree
|
164
172
|
def initialize(val)
|
165
173
|
super(val.downcase.to_sym)
|
166
174
|
end
|
@@ -187,7 +195,7 @@ module OData
|
|
187
195
|
end
|
188
196
|
|
189
197
|
# Bin ops
|
190
|
-
class BinopTree
|
198
|
+
class BinopTree
|
191
199
|
def initialize(val)
|
192
200
|
@state = :open
|
193
201
|
super(val.downcase.to_sym)
|
@@ -201,7 +209,7 @@ module OData
|
|
201
209
|
end
|
202
210
|
end
|
203
211
|
|
204
|
-
class BinopBool
|
212
|
+
class BinopBool
|
205
213
|
# reference:
|
206
214
|
# OData v4 par 5.1.1.9 Operator Precedence
|
207
215
|
def precedence
|
@@ -224,7 +232,7 @@ module OData
|
|
224
232
|
end
|
225
233
|
end
|
226
234
|
|
227
|
-
class BinopArithm
|
235
|
+
class BinopArithm
|
228
236
|
# reference:
|
229
237
|
# OData v4 par 5.1.1.9 Operator Precedence
|
230
238
|
def precedence
|
@@ -238,14 +246,14 @@ module OData
|
|
238
246
|
end
|
239
247
|
end
|
240
248
|
|
241
|
-
# TODO different num types?
|
249
|
+
# TODO: different num types?
|
242
250
|
def edm_type
|
243
251
|
:any
|
244
252
|
end
|
245
253
|
end
|
246
254
|
|
247
255
|
# Arguments or lists
|
248
|
-
class ArgTree
|
256
|
+
class ArgTree
|
249
257
|
attr_reader :type
|
250
258
|
def initialize(val)
|
251
259
|
@type = :expression
|
@@ -279,12 +287,10 @@ module OData
|
|
279
287
|
when :Separator
|
280
288
|
if @value == '(' && tok == ',' && @state == :val
|
281
289
|
true
|
290
|
+
elsif @state == :sep
|
291
|
+
[false, Parser::ErrorInvalidToken.new(tok, typ, self)]
|
282
292
|
else
|
283
|
-
|
284
|
-
[false, Parser::ErrorInvalidToken.new(tok, typ, self)]
|
285
|
-
else
|
286
|
-
true
|
287
|
-
end
|
293
|
+
true
|
288
294
|
end
|
289
295
|
when :Literal, :Qualit, :QString, :FuncTree, :FPNumber
|
290
296
|
if (@state == :open) || (@state == :sep)
|
@@ -309,7 +315,7 @@ module OData
|
|
309
315
|
end
|
310
316
|
|
311
317
|
# Numbers (floating point, ints, dec)
|
312
|
-
class FPNumber
|
318
|
+
class FPNumber
|
313
319
|
def accept?(tok, typ)
|
314
320
|
case typ
|
315
321
|
when :Delimiter, :Separator, :BinopBool, :BinopArithm
|
@@ -326,7 +332,7 @@ module OData
|
|
326
332
|
end
|
327
333
|
|
328
334
|
# Literals are unquoted words without /
|
329
|
-
class Literal
|
335
|
+
class Literal
|
330
336
|
def accept?(tok, typ)
|
331
337
|
case typ
|
332
338
|
when :Delimiter, :Separator, :BinopBool, :BinopArithm
|
@@ -339,33 +345,43 @@ module OData
|
|
339
345
|
def edm_type
|
340
346
|
:any
|
341
347
|
end
|
348
|
+
|
349
|
+
# error, Literal are leaves
|
350
|
+
# when the child is a IdentityFuncTree then this looks like
|
351
|
+
# an attempt to use a unknown function, eg. ceil(Total)
|
352
|
+
# instead of ceiling(Total)
|
353
|
+
def attach(child)
|
354
|
+
if child.kind_of? OData::Filter::IdentityFuncTree
|
355
|
+
raise Parser::ErrorInvalidFunction.new("Error in $filter expr.: invalid function #{self.value}")
|
356
|
+
else
|
357
|
+
super
|
358
|
+
end
|
359
|
+
end
|
342
360
|
end
|
343
361
|
|
344
362
|
# Qualit (qualified lits) are words separated by /
|
345
363
|
# path/path/path/attrib
|
346
|
-
class Qualit
|
364
|
+
class Qualit
|
347
365
|
REGEXP = /((?:\w+\/)+)(\w+)/.freeze
|
348
366
|
attr_reader :path
|
349
367
|
attr_reader :attrib
|
350
368
|
def initialize(val)
|
351
369
|
super(val)
|
352
370
|
# split into path + attrib
|
353
|
-
|
354
|
-
|
355
|
-
|
356
|
-
|
357
|
-
raise Parser::Error.new(self, Qualit)
|
358
|
-
end
|
371
|
+
raise Parser::Error.new(self, Qualit) unless (md = REGEXP.match(val))
|
372
|
+
|
373
|
+
@path = md[1].chomp('/')
|
374
|
+
@attrib = md[2]
|
359
375
|
end
|
360
376
|
end
|
361
377
|
|
362
378
|
# Quoted Strings
|
363
|
-
class QString
|
364
|
-
|
365
|
-
|
379
|
+
class QString
|
380
|
+
DBL_QO = "''".freeze
|
381
|
+
SI_QO = "'".freeze
|
366
382
|
def initialize(val)
|
367
383
|
# unescape double quotes
|
368
|
-
super(val.gsub(
|
384
|
+
super(val.gsub(DBL_QO, SI_QO))
|
369
385
|
end
|
370
386
|
|
371
387
|
def accept?(tok, typ)
|
@@ -1,11 +1,29 @@
|
|
1
1
|
require 'json'
|
2
|
-
require_relative '../
|
2
|
+
require_relative '../safrano/core.rb'
|
3
3
|
require_relative './entity.rb'
|
4
4
|
|
5
5
|
module OData
|
6
|
+
# remove the relation between entity and parent by clearing
|
7
|
+
# the FK field(s) (if allowed)
|
8
|
+
def self.remove_nav_relation(assoc, parent)
|
9
|
+
return unless assoc
|
10
|
+
|
11
|
+
return unless assoc[:type] == :many_to_one
|
12
|
+
|
13
|
+
# removes/clear the FK values in parent
|
14
|
+
# thus deleting the "link" between the entity and the parent
|
15
|
+
# Note: This is called if we have to delete the child--> can only be
|
16
|
+
# done after removing the FK in parent (if allowed!)
|
17
|
+
lks = [assoc[:key]].flatten
|
18
|
+
lks.each do |lk|
|
19
|
+
parent.set(lk => nil)
|
20
|
+
parent.save(transaction: false)
|
21
|
+
end
|
22
|
+
end
|
23
|
+
|
6
24
|
# link newly created entities(child) to an existing parent
|
7
25
|
# by following the association_reflection rules
|
8
|
-
def
|
26
|
+
def self.create_nav_relation(child, assoc, parent)
|
9
27
|
return unless assoc
|
10
28
|
|
11
29
|
# Note: this coding shares some bits from our sequel/plugins/join_by_paths,
|
@@ -24,16 +42,16 @@ module OData
|
|
24
42
|
lks = [leftm.primary_key].flatten
|
25
43
|
rks = [assoc[:key]].flatten
|
26
44
|
join_cond = rks.zip(lks).to_h
|
27
|
-
join_cond.each
|
45
|
+
join_cond.each do |rk, lk|
|
28
46
|
if child.values[rk] # FK in new entity from payload not nil, only check consistency
|
29
47
|
# with the parent - id(s)
|
30
|
-
if (child.values[rk] != parent.pk_hash[lk]) # error...
|
31
|
-
|
32
|
-
end
|
48
|
+
# if (child.values[rk] != parent.pk_hash[lk]) # error...
|
49
|
+
# TODO
|
50
|
+
# end
|
33
51
|
else # we can set the FK value, thus creating the "link"
|
34
52
|
child.set(rk => parent.pk_hash[lk])
|
35
53
|
end
|
36
|
-
|
54
|
+
end
|
37
55
|
when :many_to_one
|
38
56
|
# sets the FK values in parent to corresponding related child key-values
|
39
57
|
# thus creating the "link" between the new entity and the parent
|
@@ -42,31 +60,37 @@ module OData
|
|
42
60
|
lks = [assoc[:key]].flatten
|
43
61
|
rks = [child.class.primary_key].flatten
|
44
62
|
join_cond = rks.zip(lks).to_h
|
45
|
-
join_cond.each
|
63
|
+
join_cond.each do |rk, lk|
|
46
64
|
if parent.values[lk] # FK in parent not nil, only check consistency
|
47
65
|
# with the child - id(s)
|
48
|
-
if
|
49
|
-
|
50
|
-
end
|
66
|
+
# if parent.values[lk] != child.pk_hash[rk] # error...
|
67
|
+
# TODO
|
68
|
+
# end
|
51
69
|
else # we can set the FK value, thus creating the "link"
|
52
70
|
parent.set(lk => child.pk_hash[rk])
|
53
71
|
end
|
54
|
-
|
72
|
+
end
|
73
|
+
end
|
74
|
+
end
|
75
|
+
|
76
|
+
module EntityBase
|
77
|
+
module NavigationInfo
|
78
|
+
attr_reader :nav_parent
|
79
|
+
attr_reader :navattr_reflection
|
80
|
+
attr_reader :nav_name
|
81
|
+
def set_relation_info(parent, name)
|
82
|
+
@nav_parent = parent
|
83
|
+
@nav_name = name
|
84
|
+
@navattr_reflection = parent.class.association_reflections[name.to_sym]
|
85
|
+
@nav_klass = @navattr_reflection[:class_name].constantize
|
86
|
+
end
|
55
87
|
end
|
56
88
|
end
|
57
89
|
|
58
90
|
# Represents a named but nil-valued navigation-attribute of an Entity
|
59
91
|
# (usually resulting from a NULL FK db value)
|
60
92
|
class NilNavigationAttribute
|
61
|
-
|
62
|
-
attr_reader :parent
|
63
|
-
def initialize(parent, name)
|
64
|
-
@parent = parent
|
65
|
-
@name = name
|
66
|
-
@navattr_reflection = parent.class.association_reflections[name.to_sym]
|
67
|
-
@klass = @navattr_reflection[:class_name].constantize
|
68
|
-
end
|
69
|
-
|
93
|
+
include EntityBase::NavigationInfo
|
70
94
|
def odata_get(req)
|
71
95
|
if req.walker.media_value
|
72
96
|
OData::ErrorNotFound.odata_get
|
@@ -80,23 +104,30 @@ module OData
|
|
80
104
|
# create the nav. entity
|
81
105
|
def odata_post(req)
|
82
106
|
# delegate to the class method
|
83
|
-
@
|
107
|
+
@nav_klass.odata_create_entity_and_relation(req,
|
108
|
+
@navattr_reflection,
|
109
|
+
@nav_parent)
|
84
110
|
end
|
85
111
|
|
86
112
|
# create the nav. entity
|
87
113
|
def odata_put(req)
|
114
|
+
# if req.walker.raw_value
|
88
115
|
# delegate to the class method
|
89
|
-
@
|
116
|
+
@nav_klass.odata_create_entity_and_relation(req,
|
117
|
+
@navattr_reflection,
|
118
|
+
@nav_parent)
|
119
|
+
# else
|
120
|
+
# end
|
90
121
|
end
|
91
122
|
|
92
123
|
# empty output as OData json (v2)
|
93
124
|
def to_odata_json(*)
|
94
|
-
{ 'd' =>
|
125
|
+
{ 'd' => EMPTY_HASH }.to_json
|
95
126
|
end
|
96
127
|
|
97
128
|
# for testing purpose (assert_equal ...)
|
98
129
|
def ==(other)
|
99
|
-
(@
|
130
|
+
(@nav_parent == other.nav_parent) && (@nav_name == other.nav_name)
|
100
131
|
end
|
101
132
|
|
102
133
|
# methods related to transitions to next state (cf. walker)
|
@@ -109,9 +140,11 @@ module OData
|
|
109
140
|
[self, :end_with_value]
|
110
141
|
end
|
111
142
|
|
143
|
+
ALLOWED_TRANSITIONS = [Safrano::TransitionEnd,
|
144
|
+
Safrano::TransitionValue].freeze
|
145
|
+
|
112
146
|
def allowed_transitions
|
113
|
-
|
114
|
-
Safrano::TransitionValue]
|
147
|
+
ALLOWED_TRANSITIONS
|
115
148
|
end
|
116
149
|
end
|
117
150
|
include Transitions
|