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