ruote 2.1.7 → 2.1.8
Sign up to get free protection for your applications and to get access to all the features.
- data/CHANGELOG.txt +13 -0
- data/CREDITS.txt +2 -1
- data/Rakefile +4 -3
- data/TODO.txt +11 -1
- data/lib/ruote/exp/fe_concurrence.rb +26 -2
- data/lib/ruote/exp/fe_participant.rb +1 -1
- data/lib/ruote/exp/merge.rb +16 -9
- data/lib/ruote/fei.rb +1 -1
- data/lib/ruote/parser.rb +2 -1
- data/lib/ruote/part/smtp_participant.rb +22 -41
- data/lib/ruote/part/storage_participant.rb +20 -1
- data/lib/ruote/part/template.rb +34 -18
- data/lib/ruote/storage/base.rb +2 -20
- data/lib/ruote/storage/composite_storage.rb +134 -0
- data/lib/ruote/storage/fs_storage.rb +15 -1
- data/lib/ruote/util/dollar.rb +1 -0
- data/lib/ruote/util/misc.rb +5 -1
- data/lib/ruote/util/time.rb +13 -3
- data/lib/ruote/version.rb +1 -1
- data/lib/ruote/worker.rb +2 -2
- data/ruote.gemspec +18 -12
- data/test/bm/seq_thousand.rb +1 -1
- data/test/functional/base.rb +6 -8
- data/test/functional/crunner.rb +2 -0
- data/test/functional/eft_0_process_definition.rb +2 -2
- data/test/functional/eft_10_cancel_process.rb +2 -2
- data/test/functional/eft_11_wait.rb +3 -3
- data/test/functional/eft_12_listen.rb +1 -1
- data/test/functional/eft_13_iterator.rb +14 -14
- data/test/functional/eft_14_cursor.rb +12 -12
- data/test/functional/eft_15_loop.rb +2 -2
- data/test/functional/eft_16_if.rb +11 -15
- data/test/functional/eft_17_equals.rb +2 -2
- data/test/functional/eft_18_concurrent_iterator.rb +35 -8
- data/test/functional/eft_1_echo.rb +1 -1
- data/test/functional/eft_21_restore.rb +3 -3
- data/test/functional/eft_22_noop.rb +1 -1
- data/test/functional/eft_23_apply.rb +9 -9
- data/test/functional/eft_25_command.rb +1 -1
- data/test/functional/eft_27_inc.rb +12 -12
- data/test/functional/eft_28_when.rb +4 -4
- data/test/functional/eft_2_sequence.rb +3 -3
- data/test/functional/eft_3_participant.rb +5 -5
- data/test/functional/eft_4_set.rb +12 -12
- data/test/functional/eft_5_subprocess.rb +8 -8
- data/test/functional/eft_6_concurrence.rb +17 -4
- data/test/functional/eft_7_forget.rb +1 -1
- data/test/functional/eft_8_undo.rb +3 -3
- data/test/functional/ft_0_worker.rb +17 -1
- data/test/functional/ft_10_dollar.rb +9 -9
- data/test/functional/ft_11_recursion.rb +2 -2
- data/test/functional/ft_13_variables.rb +4 -4
- data/test/functional/ft_17_conditional.rb +5 -5
- data/test/functional/ft_19_alias.rb +1 -1
- data/test/functional/ft_1_process_status.rb +1 -1
- data/test/functional/ft_20_storage_participant.rb +2 -0
- data/test/functional/ft_22_process_definitions.rb +11 -4
- data/test/functional/ft_24_block_participants.rb +13 -4
- data/test/functional/ft_27_var_indirection.rb +5 -5
- data/test/functional/ft_28_null_noop_participants.rb +1 -1
- data/test/functional/ft_29_part_template.rb +4 -23
- data/test/functional/ft_30_smtp_participant.rb +50 -4
- data/test/functional/ft_32_fs_history.rb +4 -8
- data/test/functional/ft_33_participant_subprocess_priority.rb +1 -1
- data/test/functional/ft_36_storage_history.rb +2 -2
- data/test/functional/ft_5_on_error.rb +5 -5
- data/test/functional/ft_8_participant_consumption.rb +2 -2
- data/test/path_helper.rb +1 -1
- data/test/test_helper.rb +17 -0
- data/test/unit/storage.rb +2 -17
- data/test/unit/ut_19_part_template.rb +76 -0
- data/test/unit/ut_1_fei.rb +13 -0
- data/test/unit/ut_20_composite_storage.rb +34 -0
- metadata +111 -55
data/CHANGELOG.txt
CHANGED
@@ -2,6 +2,19 @@
|
|
2
2
|
= ruote - CHANGELOG.txt
|
3
3
|
|
4
4
|
|
5
|
+
== ruote - 2.1.8 released 2010/03/15
|
6
|
+
|
7
|
+
- participant#schedule_timeout workaround for issue with JRuby 1.4.0 (1.8.7)
|
8
|
+
- implemented Ruote::CompositeStorage
|
9
|
+
- leveraging rufus-cloche 0.1.16 and the 'cloche_nolock' option (FsStorage)
|
10
|
+
- SmtpParticipant and ruote/part/template.rb reorganization
|
11
|
+
- StorageParticipant when returned by engine#register was unusable. Fixed.
|
12
|
+
- string keys for SmtpParticipant. Thanks Gonzalo
|
13
|
+
- fixed every('10m') bug. Thanks Gonzalo Suarez
|
14
|
+
- Ruote::FlowExpressionId.from_id(s) more permissive
|
15
|
+
- concurrence (and concurrent-iterator) :merge_type => :stack
|
16
|
+
|
17
|
+
|
5
18
|
== ruote - 2.1.7 released 2010/02/15
|
6
19
|
|
7
20
|
- now works on WinXP, Ruby 1.8.7
|
data/CREDITS.txt
CHANGED
data/Rakefile
CHANGED
@@ -24,15 +24,16 @@ ruote is an open source ruby workflow engine.
|
|
24
24
|
gem.test_file = 'test/test.rb'
|
25
25
|
|
26
26
|
gem.add_dependency 'rufus-json', '>= 0.2.0'
|
27
|
-
gem.add_dependency 'rufus-cloche', '>= 0.1.
|
27
|
+
gem.add_dependency 'rufus-cloche', '>= 0.1.16'
|
28
28
|
gem.add_dependency 'rufus-dollar'
|
29
29
|
gem.add_dependency 'rufus-lru'
|
30
30
|
gem.add_dependency 'rufus-mnemo', '>= 1.1.0'
|
31
|
-
gem.add_dependency 'rufus-scheduler', '>= 2.0.
|
31
|
+
gem.add_dependency 'rufus-scheduler', '>= 2.0.5'
|
32
32
|
gem.add_dependency 'rufus-treechecker', '>= 1.0.3'
|
33
33
|
|
34
|
-
gem.add_development_dependency '
|
34
|
+
gem.add_development_dependency 'rake'
|
35
35
|
gem.add_development_dependency 'yard'
|
36
|
+
gem.add_development_dependency 'json'
|
36
37
|
gem.add_development_dependency 'builder'
|
37
38
|
gem.add_development_dependency 'mailtrap'
|
38
39
|
gem.add_development_dependency 'jeweler'
|
data/TODO.txt
CHANGED
@@ -191,6 +191,8 @@
|
|
191
191
|
[o] StorageParticipant#query(wfid, participant_name, {fields})
|
192
192
|
[x] break fs_history, prepare for dm_history
|
193
193
|
[o] part = engine.register_participant :alpha, StorageParticipant should work...
|
194
|
+
[o] concurrence :merge_type => 'stack'
|
195
|
+
[o] CompositeStorage.new('msgs' => AmqpStorage.new(''), ...)
|
194
196
|
|
195
197
|
[ ] exp : exp (restricted form of eval ?)
|
196
198
|
[ ] exp : case (is it necessary ?)
|
@@ -301,9 +303,17 @@
|
|
301
303
|
[ ] implement pause engine
|
302
304
|
[ ] implement pause process
|
303
305
|
|
304
|
-
[ ] engine.on_error = 'participant_name'
|
306
|
+
[ ] engine.on_error = 'participant_name' // 'subprocess_name'
|
305
307
|
|
306
308
|
[ ] "business days" plugin
|
307
309
|
|
308
310
|
[ ] issue with ruote-kit and inpa participants...
|
309
311
|
|
312
|
+
[ ] let the storage participant leverage Ruote::FlowExpressionId.from_id(s)
|
313
|
+
|
314
|
+
[ ] participant :ref => '${f:nada}', :or => 'xyz'
|
315
|
+
(look at OpenWFE manual, this feature already existed in there)
|
316
|
+
http://www.openwfe.org/manual/ch06s02.html#expression_participant
|
317
|
+
else-ref... list of participants...
|
318
|
+
ref="alpha && bravo", ref="alpha||bravo" (|| parallel :( )
|
319
|
+
|
@@ -90,7 +90,7 @@ module Ruote::Exp
|
|
90
90
|
# highest and lowest refer to the position in the list of branch. It's useful
|
91
91
|
# to set a fixed winner.
|
92
92
|
#
|
93
|
-
# concurrence :merge => highest do
|
93
|
+
# concurrence :merge => :highest do
|
94
94
|
# alpha
|
95
95
|
# bravo
|
96
96
|
# end
|
@@ -109,6 +109,30 @@ module Ruote::Exp
|
|
109
109
|
# a new field for each branch. The name of each field is the index of the
|
110
110
|
# branch from '0' to ...
|
111
111
|
#
|
112
|
+
# :stack will stack the workitems coming back from the concurrence branches
|
113
|
+
# in an array whose order is determined by the :merge attributes. The array
|
114
|
+
# is placed in the 'stack' field of the resulting workitem.
|
115
|
+
# Note that the :stack merge_type also creates a 'stack_attributes' field
|
116
|
+
# and populates it with the expanded attributes of the concurrence.
|
117
|
+
#
|
118
|
+
# Thus
|
119
|
+
#
|
120
|
+
# sequence do
|
121
|
+
# concurrence :merge => :highest, :merge_type => :stack do
|
122
|
+
# reviewer1
|
123
|
+
# reviewer2
|
124
|
+
# end
|
125
|
+
# editor
|
126
|
+
# end
|
127
|
+
#
|
128
|
+
# will see the 'editor' receive a workitem whose fields look like :
|
129
|
+
#
|
130
|
+
# { 'stack' => [{ ... reviewer1 fields ... }, { ... reviewer2 fields ... }],
|
131
|
+
# 'stack_attributes' => { 'merge'=> 'highest', 'merge_type' => 'stack' } }
|
132
|
+
#
|
133
|
+
# This could prove useful for participant having to deal with multiple merge
|
134
|
+
# strategy results.
|
135
|
+
#
|
112
136
|
#
|
113
137
|
# === :over_if (and :over_unless)
|
114
138
|
#
|
@@ -140,7 +164,7 @@ module Ruote::Exp
|
|
140
164
|
h.ccount = nil if h.ccount < 1
|
141
165
|
|
142
166
|
h.cmerge = att(:merge, %w[ first last highest lowest ])
|
143
|
-
h.cmerge_type = att(:merge_type, %w[ override mix isolate ])
|
167
|
+
h.cmerge_type = att(:merge_type, %w[ override mix isolate stack ])
|
144
168
|
h.remaining = att(:remaining, %w[ cancel forget ])
|
145
169
|
|
146
170
|
h.workitems = (h.cmerge == 'first' || h.cmerge == 'last') ? [] : {}
|
@@ -201,7 +201,7 @@ module Ruote::Exp
|
|
201
201
|
|
202
202
|
timeout =
|
203
203
|
attribute(:timeout) ||
|
204
|
-
(p_info.
|
204
|
+
(p_info.timeout rescue nil) ||
|
205
205
|
(p_info.is_a?(Array) ? p_info.last['timeout'] : nil)
|
206
206
|
|
207
207
|
do_schedule_timeout(timeout)
|
data/lib/ruote/exp/merge.rb
CHANGED
@@ -45,18 +45,25 @@ module Ruote::Exp
|
|
45
45
|
|
46
46
|
return source if type == 'override'
|
47
47
|
|
48
|
-
|
49
|
-
|
50
|
-
|
51
|
-
|
52
|
-
|
48
|
+
if target == nil
|
49
|
+
case type
|
50
|
+
when 'isolate'
|
51
|
+
source['fields'] = { index.to_s => source['fields'] }
|
52
|
+
when 'stack'
|
53
|
+
source['fields'] = { 'stack' => [ source['fields'] ] }
|
54
|
+
end
|
55
|
+
end
|
53
56
|
|
54
57
|
return source unless target
|
55
58
|
|
56
|
-
|
57
|
-
|
58
|
-
|
59
|
-
|
59
|
+
case type
|
60
|
+
when 'mix'
|
61
|
+
target['fields'].merge!(source['fields'])
|
62
|
+
when 'stack'
|
63
|
+
target['fields']['stack'] << source['fields']
|
64
|
+
target['fields']['stack_attributes'] = expand_atts
|
65
|
+
else # 'isolate'
|
66
|
+
target['fields'][index.to_s] = source['fields']
|
60
67
|
end
|
61
68
|
|
62
69
|
target
|
data/lib/ruote/fei.rb
CHANGED
@@ -92,7 +92,7 @@ module Ruote
|
|
92
92
|
|
93
93
|
FlowExpressionId.new(
|
94
94
|
'engine_id' => engine_id,
|
95
|
-
'expid' => ss[
|
95
|
+
'expid' => ss[-3], 'sub_wfid' => ss[-2], 'wfid' => ss[-1])
|
96
96
|
end
|
97
97
|
|
98
98
|
# Returns the last number in the expid. For instance, if the expid is
|
data/lib/ruote/parser.rb
CHANGED
@@ -65,7 +65,8 @@ module Ruote
|
|
65
65
|
end
|
66
66
|
|
67
67
|
raise ArgumentError.new(
|
68
|
-
"doesn't know how to parse definition (#{definition.class})"
|
68
|
+
"doesn't know how to parse definition (#{definition.class}) " +
|
69
|
+
"or error in process definition")
|
69
70
|
end
|
70
71
|
|
71
72
|
# Class method for parsing process definition (XML, Ruby, from file or
|
@@ -31,8 +31,7 @@ require 'ruote/part/template'
|
|
31
31
|
module Ruote
|
32
32
|
|
33
33
|
#
|
34
|
-
# A very stupid SMTP participant
|
35
|
-
# messages. This class is meant as a base for more complex email participants.
|
34
|
+
# A very stupid SMTP participant.
|
36
35
|
#
|
37
36
|
# == options
|
38
37
|
#
|
@@ -43,10 +42,11 @@ module Ruote
|
|
43
42
|
# * :template - a String template for the mail message
|
44
43
|
# * :notification - when set to true, the flow will resume immediately after having sent the email
|
45
44
|
#
|
45
|
+
#
|
46
46
|
# == :template
|
47
47
|
#
|
48
48
|
# @engine.register_participant(
|
49
|
-
# :no_good_notification
|
49
|
+
# :no_good_notification,
|
50
50
|
# Ruote::SmtpParticipant,
|
51
51
|
# :server => 'smtp.example.com'
|
52
52
|
# :port => 25,
|
@@ -59,33 +59,18 @@ module Ruote
|
|
59
59
|
# process definitions (in this example, the workitem field email_subject will
|
60
60
|
# be used as the subject of the email...)
|
61
61
|
#
|
62
|
-
# == block template
|
63
|
-
#
|
64
|
-
# Whereas the :template option accepts a String, the block template may
|
65
|
-
# be useful when more complex templates are to be computed.
|
66
62
|
#
|
67
|
-
#
|
68
|
-
# :no_good_notification
|
69
|
-
# Ruote::SmtpParticipant,
|
70
|
-
# :server => 'smtp.example.com'
|
71
|
-
# :port => 25,
|
72
|
-
# :to => 'toto@example.com',
|
73
|
-
# :from => 'john@example.com',
|
74
|
-
# :notification => true
|
75
|
-
# ) do
|
63
|
+
# == :to or workitem.fields['email_target']
|
76
64
|
#
|
77
|
-
#
|
78
|
-
#
|
79
|
-
#
|
80
|
-
# s << "Subject: ${f:email_subject}\n"
|
81
|
-
# s << ""
|
82
|
-
# 3.times { s << "this is no good." }
|
65
|
+
# The target of the email is either given via the workitem field
|
66
|
+
# 'email_target', either by the option :to. The workitem field takes
|
67
|
+
# precedence if both are present.
|
83
68
|
#
|
84
|
-
#
|
85
|
-
#
|
69
|
+
# This parameter/option may be either a single (string) email address, either
|
70
|
+
# an array of (string) email addresses.
|
86
71
|
#
|
87
72
|
#
|
88
|
-
# == mail listener
|
73
|
+
# == final note : mail listener
|
89
74
|
#
|
90
75
|
# This participant cannot read POP/IMAP accounts for you. You have to
|
91
76
|
# use a mail listener or get a web reply by placing a link in the message...
|
@@ -95,33 +80,29 @@ module Ruote
|
|
95
80
|
include LocalParticipant
|
96
81
|
include TemplateMixin
|
97
82
|
|
98
|
-
def initialize (opts
|
99
|
-
|
100
|
-
@server = opts[:server] || '127.0.0.1'
|
101
|
-
@port = opts[:port] || 25
|
83
|
+
def initialize (opts)
|
102
84
|
|
103
|
-
@
|
104
|
-
@to = opts[:to]
|
105
|
-
|
106
|
-
@template = opts[:template]
|
107
|
-
@block_template = block
|
108
|
-
|
109
|
-
@notification = opts[:notification]
|
85
|
+
@opts = opts.inject({}) { |h, (k, v)| h[k.to_s] = v; h }
|
110
86
|
end
|
111
87
|
|
112
88
|
def consume (workitem)
|
113
89
|
|
114
|
-
to = workitem.fields['email_target'] || @to
|
90
|
+
to = workitem.fields['email_target'] || @opts['to']
|
115
91
|
to = Array(to)
|
116
92
|
|
117
93
|
text = render_template(
|
118
|
-
|
94
|
+
@opts['template'],
|
95
|
+
Ruote::Exp::FlowExpression.fetch(@context, workitem.fei.to_h),
|
96
|
+
workitem)
|
97
|
+
|
98
|
+
server = @opts['server'] || '127.0.0.1'
|
99
|
+
port = @opts['port'] || 25
|
119
100
|
|
120
|
-
Net::SMTP.start(
|
121
|
-
smtp.send_message(text, @from, *to)
|
101
|
+
Net::SMTP.start(server, port) do |smtp|
|
102
|
+
smtp.send_message(text, @opts['from'] || 'ruote@example.org', *to)
|
122
103
|
end
|
123
104
|
|
124
|
-
reply_to_engine(workitem) if @notification
|
105
|
+
reply_to_engine(workitem) if @opts['notification']
|
125
106
|
end
|
126
107
|
|
127
108
|
def cancel (fei, flavour)
|
@@ -31,7 +31,24 @@ module Ruote
|
|
31
31
|
# A participant that stores the workitem in the same storage used by the
|
32
32
|
# engine and the worker(s).
|
33
33
|
#
|
34
|
-
#
|
34
|
+
# part = engine.register_participant 'alfred', Ruote::StorageParticipant
|
35
|
+
#
|
36
|
+
# # ... a bit later
|
37
|
+
#
|
38
|
+
# puts "workitems still open : "
|
39
|
+
# part.each do |workitem|
|
40
|
+
# puts "#{workitem.fei.wfid} - #{workitem.fields['params']['task']}"
|
41
|
+
# end
|
42
|
+
#
|
43
|
+
# # ... when done with a workitem
|
44
|
+
#
|
45
|
+
# part.reply(workitem)
|
46
|
+
# # this will remove the workitem from the storage and hand it back
|
47
|
+
# # to the engine
|
48
|
+
#
|
49
|
+
# Does not thread by default (the engine will not spawn a dedicated thread
|
50
|
+
# to handle the delivery to this participant, the workitem will get stored
|
51
|
+
# via the main engine thread and basta).
|
35
52
|
#
|
36
53
|
class StorageParticipant
|
37
54
|
|
@@ -44,6 +61,8 @@ module Ruote
|
|
44
61
|
|
45
62
|
if engine_or_options.respond_to?(:context)
|
46
63
|
@context = engine_or_options.context
|
64
|
+
elsif engine_or_options.is_a?(Ruote::Context)
|
65
|
+
@context = engine_or_options
|
47
66
|
else
|
48
67
|
options = engine_or_options
|
49
68
|
end
|
data/lib/ruote/part/template.rb
CHANGED
@@ -22,41 +22,57 @@
|
|
22
22
|
# Made in Japan.
|
23
23
|
#++
|
24
24
|
|
25
|
+
require 'rufus/json'
|
26
|
+
require 'ruote/util/dollar'
|
27
|
+
|
25
28
|
|
26
29
|
module Ruote
|
27
30
|
|
28
31
|
#
|
29
|
-
#
|
30
|
-
#
|
32
|
+
# Template rendering helper.
|
33
|
+
#
|
34
|
+
# (Currently only used by the SmtpParticipant, could prove useful for
|
35
|
+
# custom participants)
|
31
36
|
#
|
32
37
|
module TemplateMixin
|
33
38
|
|
34
|
-
def render_template (flow_expression, workitem)
|
39
|
+
def render_template (template, flow_expression, workitem)
|
35
40
|
|
36
|
-
template = if
|
41
|
+
template = (File.read(template) rescue nil) if is_a_file?(template)
|
37
42
|
|
38
|
-
|
39
|
-
when 1 then @block_template.call(workitem)
|
40
|
-
when 2 then @block_template.call(workitem, flow_expression)
|
41
|
-
else @block_template.call(workitem, flow_expression, self)
|
42
|
-
end
|
43
|
+
return render_default_template(workitem) unless template
|
43
44
|
|
44
|
-
|
45
|
+
template = template.to_s
|
46
|
+
workitem = workitem.to_h if workitem.respond_to?(:to_h)
|
45
47
|
|
46
|
-
|
48
|
+
Ruote.dosub(template, flow_expression, workitem)
|
49
|
+
end
|
47
50
|
|
48
|
-
|
51
|
+
# Simply returns a pretty-printed view of the workitem
|
52
|
+
#
|
53
|
+
def render_default_template (workitem)
|
49
54
|
|
50
|
-
|
55
|
+
workitem = workitem.to_h if workitem.respond_to?(:to_h)
|
56
|
+
|
57
|
+
s = []
|
58
|
+
s << "workitem for #{workitem['participant_name']}"
|
59
|
+
s << ''
|
60
|
+
s << Rufus::Json.encode(workitem['fei'])
|
61
|
+
s << ''
|
62
|
+
workitem['fields'].keys.sort.each do |key|
|
63
|
+
s << " - '#{key}' ==> #{Rufus::Json.encode(workitem['fields'][key])}"
|
51
64
|
end
|
65
|
+
s.join("\n")
|
66
|
+
end
|
52
67
|
|
53
|
-
|
54
|
-
ArgumentError.new('no @template or @block_template found')
|
55
|
-
) unless template
|
68
|
+
protected
|
56
69
|
|
57
|
-
|
70
|
+
def is_a_file? (s)
|
58
71
|
|
59
|
-
|
72
|
+
return false unless s
|
73
|
+
return false if s.index("\n")
|
74
|
+
|
75
|
+
File.exist?(s)
|
60
76
|
end
|
61
77
|
end
|
62
78
|
end
|
data/lib/ruote/storage/base.rb
CHANGED
@@ -32,12 +32,6 @@ module Ruote
|
|
32
32
|
#
|
33
33
|
module StorageBase
|
34
34
|
|
35
|
-
def reserve (doc)
|
36
|
-
|
37
|
-
#(delete(doc) != true)
|
38
|
-
delete(doc).nil?
|
39
|
-
end
|
40
|
-
|
41
35
|
#--
|
42
36
|
# configurations
|
43
37
|
#++
|
@@ -143,8 +137,8 @@ module Ruote
|
|
143
137
|
def put_schedule (flavour, owner_fei, s, msg)
|
144
138
|
|
145
139
|
at = if s.is_a?(Time) # at or every
|
146
|
-
|
147
|
-
elsif is_cron_string(s) # cron
|
140
|
+
s
|
141
|
+
elsif Ruote.is_cron_string(s) # cron
|
148
142
|
Rufus::CronLine.new(s).next_time(Time.now + 1)
|
149
143
|
else # at or every
|
150
144
|
Ruote.s_to_at(s)
|
@@ -210,18 +204,6 @@ module Ruote
|
|
210
204
|
|
211
205
|
scheds.select { |sched| sched['at'] <= now }
|
212
206
|
end
|
213
|
-
|
214
|
-
# Waiting for a better implementation of it in rufus-scheduler 2.0.4
|
215
|
-
#
|
216
|
-
def is_cron_string (s)
|
217
|
-
|
218
|
-
ss = s.split(' ')
|
219
|
-
|
220
|
-
return false if ss.size < 5 || ss.size > 6
|
221
|
-
return false if s.match(/\d{4}/)
|
222
|
-
|
223
|
-
true
|
224
|
-
end
|
225
207
|
end
|
226
208
|
end
|
227
209
|
|