ruote 2.1.7 → 2.1.8
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.
- 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
|
|