bumbleworks-api 0.0.1
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 +7 -0
- data/.gitignore +10 -0
- data/.rspec +3 -0
- data/.ruby-version +1 -0
- data/Gemfile +3 -0
- data/LICENSE.txt +22 -0
- data/README.md +25 -0
- data/Rakefile +9 -0
- data/bumbleworks-api.gemspec +32 -0
- data/config.ru +13 -0
- data/lib/bumbleworks/api.rb +15 -0
- data/lib/bumbleworks/api/config/application.rb +10 -0
- data/lib/bumbleworks/api/config/routes.rb +34 -0
- data/lib/bumbleworks/api/controllers/application_controller.rb +6 -0
- data/lib/bumbleworks/api/controllers/entities_controller.rb +25 -0
- data/lib/bumbleworks/api/controllers/errors_controller.rb +32 -0
- data/lib/bumbleworks/api/controllers/expressions_controller.rb +29 -0
- data/lib/bumbleworks/api/controllers/processes_controller.rb +29 -0
- data/lib/bumbleworks/api/controllers/tasks_controller.rb +43 -0
- data/lib/bumbleworks/api/controllers/trackers_controller.rb +13 -0
- data/lib/bumbleworks/api/controllers/workers_controller.rb +51 -0
- data/lib/bumbleworks/api/lib/presenter.rb +32 -0
- data/lib/bumbleworks/api/lib/presenters/entity_class_presenter.rb +19 -0
- data/lib/bumbleworks/api/lib/presenters/entity_presenter.rb +13 -0
- data/lib/bumbleworks/api/lib/presenters/error_presenter.rb +15 -0
- data/lib/bumbleworks/api/lib/presenters/expression_presenter.rb +17 -0
- data/lib/bumbleworks/api/lib/presenters/process_presenter.rb +32 -0
- data/lib/bumbleworks/api/lib/presenters/task_presenter.rb +15 -0
- data/lib/bumbleworks/api/lib/presenters/tracker_presenter.rb +16 -0
- data/lib/bumbleworks/api/lib/presenters/worker_presenter.rb +30 -0
- data/lib/bumbleworks/api/lib/time_support.rb +23 -0
- data/lib/bumbleworks/api/version.rb +5 -0
- data/playground_setup.rb +30 -0
- data/spec/controllers/entities_controller_spec.rb +32 -0
- data/spec/controllers/errors_controller_spec.rb +42 -0
- data/spec/controllers/expressions_controller_spec.rb +40 -0
- data/spec/controllers/processes_controller_spec.rb +50 -0
- data/spec/controllers/tasks_controller_spec.rb +82 -0
- data/spec/controllers/trackers_controller_spec.rb +25 -0
- data/spec/controllers/workers_controller_spec.rb +113 -0
- data/spec/fixtures/bumbleworks_config.rb +10 -0
- data/spec/fixtures/entities/mock_entity.rb +38 -0
- data/spec/fixtures/entities/widget.rb +9 -0
- data/spec/fixtures/entities/widgety_fidget.rb +5 -0
- data/spec/fixtures/participants.rb +3 -0
- data/spec/fixtures/participants/naughty_participant.rb +16 -0
- data/spec/fixtures/processes/error_process.rb +3 -0
- data/spec/fixtures/processes/task_process.rb +9 -0
- data/spec/fixtures/processes/waiting_process.rb +8 -0
- data/spec/lib/presenter_spec.rb +29 -0
- data/spec/lib/presenters/entity_class_presenter_spec.rb +17 -0
- data/spec/lib/presenters/entity_presenter_spec.rb +15 -0
- data/spec/lib/presenters/error_presenter_spec.rb +18 -0
- data/spec/lib/presenters/expression_presenter_spec.rb +33 -0
- data/spec/lib/presenters/process_presenter_spec.rb +42 -0
- data/spec/lib/presenters/task_presenter_spec.rb +18 -0
- data/spec/lib/presenters/tracker_presenter_spec.rb +18 -0
- data/spec/lib/presenters/worker_presenter_spec.rb +30 -0
- data/spec/lib/time_support_spec.rb +42 -0
- data/spec/spec_helper.rb +39 -0
- data/spec/support/api_helper.rb +23 -0
- data/spec/support/process_helpers.rb +11 -0
- metadata +261 -0
@@ -0,0 +1,32 @@
|
|
1
|
+
module Bumbleworks
|
2
|
+
module Api
|
3
|
+
class Presenter
|
4
|
+
attr_reader :presented
|
5
|
+
|
6
|
+
class << self
|
7
|
+
def present(presented)
|
8
|
+
if presented.is_a?(Array)
|
9
|
+
from_array(presented)
|
10
|
+
else
|
11
|
+
new(presented)
|
12
|
+
end
|
13
|
+
end
|
14
|
+
|
15
|
+
def from_array(array)
|
16
|
+
array.map { |presented|
|
17
|
+
new(presented, in_collection: true)
|
18
|
+
}
|
19
|
+
end
|
20
|
+
end
|
21
|
+
|
22
|
+
def initialize(presented, in_collection: false)
|
23
|
+
@presented = presented
|
24
|
+
@in_collection = in_collection
|
25
|
+
end
|
26
|
+
|
27
|
+
def in_collection?
|
28
|
+
@in_collection == true
|
29
|
+
end
|
30
|
+
end
|
31
|
+
end
|
32
|
+
end
|
@@ -0,0 +1,19 @@
|
|
1
|
+
module Bumbleworks
|
2
|
+
module Api
|
3
|
+
class EntityClassPresenter < Presenter
|
4
|
+
def to_hash
|
5
|
+
{
|
6
|
+
:class => presented.name,
|
7
|
+
:count => presented.count,
|
8
|
+
:registered_processes => registered_processes
|
9
|
+
}
|
10
|
+
end
|
11
|
+
|
12
|
+
def registered_processes
|
13
|
+
presented.processes.map { |name, attributes|
|
14
|
+
attributes.merge(:name => name)
|
15
|
+
}
|
16
|
+
end
|
17
|
+
end
|
18
|
+
end
|
19
|
+
end
|
@@ -0,0 +1,15 @@
|
|
1
|
+
module Bumbleworks
|
2
|
+
module Api
|
3
|
+
class ErrorPresenter < Presenter
|
4
|
+
def to_hash
|
5
|
+
{
|
6
|
+
:process_id => presented.wfid,
|
7
|
+
:expression_id => presented.expression.expid,
|
8
|
+
:error_class_name => presented.error_class_name,
|
9
|
+
:message => presented.message,
|
10
|
+
:backtrace => presented.backtrace
|
11
|
+
}
|
12
|
+
end
|
13
|
+
end
|
14
|
+
end
|
15
|
+
end
|
@@ -0,0 +1,17 @@
|
|
1
|
+
module Bumbleworks
|
2
|
+
module Api
|
3
|
+
class ExpressionPresenter < Presenter
|
4
|
+
def to_hash
|
5
|
+
hash = {
|
6
|
+
:process_id => presented.process.id,
|
7
|
+
:expression_id => presented.expid,
|
8
|
+
:tree => presented.tree
|
9
|
+
}
|
10
|
+
if presented.error
|
11
|
+
hash.merge!(:error => ErrorPresenter.present(presented.error).to_hash)
|
12
|
+
end
|
13
|
+
hash
|
14
|
+
end
|
15
|
+
end
|
16
|
+
end
|
17
|
+
end
|
@@ -0,0 +1,32 @@
|
|
1
|
+
module Bumbleworks
|
2
|
+
module Api
|
3
|
+
class ProcessPresenter < Presenter
|
4
|
+
def to_hash
|
5
|
+
{
|
6
|
+
:id => presented.id,
|
7
|
+
:definition_name => presented.definition_name,
|
8
|
+
:subscribed_events => presented.subscribed_events,
|
9
|
+
:entity_name => presented.entity_name
|
10
|
+
}.merge(extras)
|
11
|
+
end
|
12
|
+
|
13
|
+
def extras
|
14
|
+
if in_collection?
|
15
|
+
{}
|
16
|
+
else
|
17
|
+
{
|
18
|
+
:original_tree => presented.original_tree
|
19
|
+
}.merge(entity_hash)
|
20
|
+
end
|
21
|
+
end
|
22
|
+
|
23
|
+
def entity_hash
|
24
|
+
if presented.entity
|
25
|
+
{ :entity => EntityPresenter.present(presented.entity).to_hash }
|
26
|
+
else
|
27
|
+
{}
|
28
|
+
end
|
29
|
+
end
|
30
|
+
end
|
31
|
+
end
|
32
|
+
end
|
@@ -0,0 +1,15 @@
|
|
1
|
+
module Bumbleworks
|
2
|
+
module Api
|
3
|
+
class TaskPresenter < Presenter
|
4
|
+
def to_hash
|
5
|
+
{
|
6
|
+
:id => presented.id,
|
7
|
+
:name => presented.to_s,
|
8
|
+
:role => presented.role,
|
9
|
+
:claimant => presented.claimant,
|
10
|
+
:process_id => presented.wfid
|
11
|
+
}
|
12
|
+
end
|
13
|
+
end
|
14
|
+
end
|
15
|
+
end
|
@@ -0,0 +1,16 @@
|
|
1
|
+
module Bumbleworks
|
2
|
+
module Api
|
3
|
+
class TrackerPresenter < Presenter
|
4
|
+
def to_hash
|
5
|
+
{
|
6
|
+
:id => presented.id,
|
7
|
+
:waiting_expression => presented.waiting_expression,
|
8
|
+
:original_hash => presented.original_hash,
|
9
|
+
:action => presented.action,
|
10
|
+
:conditions => presented.conditions,
|
11
|
+
:process_id => presented.wfid
|
12
|
+
}
|
13
|
+
end
|
14
|
+
end
|
15
|
+
end
|
16
|
+
end
|
@@ -0,0 +1,30 @@
|
|
1
|
+
module Bumbleworks
|
2
|
+
module Api
|
3
|
+
class WorkerPresenter < Presenter
|
4
|
+
def to_hash
|
5
|
+
{
|
6
|
+
:id => presented.id,
|
7
|
+
:pid => presented.pid,
|
8
|
+
:name => presented.name,
|
9
|
+
:state => presented.state,
|
10
|
+
:ip => presented.ip,
|
11
|
+
:hostname => presented.hostname,
|
12
|
+
:system => presented.system,
|
13
|
+
:launched_at => presented.launched_at,
|
14
|
+
:updated_at => presented.updated_at,
|
15
|
+
:worker_class => presented.worker_class_name,
|
16
|
+
:uptime => presented.uptime,
|
17
|
+
:uptime_in_words => uptime_in_words,
|
18
|
+
:processed_last_minute => presented.processed_last_minute,
|
19
|
+
:wait_time_last_minute => presented.wait_time_last_minute,
|
20
|
+
:processed_last_hour => presented.processed_last_hour,
|
21
|
+
:wait_time_last_hour => presented.wait_time_last_hour
|
22
|
+
}
|
23
|
+
end
|
24
|
+
|
25
|
+
def uptime_in_words
|
26
|
+
TimeSupport.seconds_to_units_in_words(presented.uptime)
|
27
|
+
end
|
28
|
+
end
|
29
|
+
end
|
30
|
+
end
|
@@ -0,0 +1,23 @@
|
|
1
|
+
module TimeSupport
|
2
|
+
class << self
|
3
|
+
def seconds_to_units(seconds, include_zeros: false, round_seconds: true)
|
4
|
+
seconds = seconds.to_i if round_seconds
|
5
|
+
units = Hash[
|
6
|
+
[:days, :hours, :minutes, :seconds].zip(
|
7
|
+
[60, 60, 24].inject([seconds]) {|result, unitsize|
|
8
|
+
result.unshift(*result.shift.divmod(unitsize))
|
9
|
+
result
|
10
|
+
}
|
11
|
+
)
|
12
|
+
]
|
13
|
+
units.reject! { |k,v| v == 0 } unless include_zeros
|
14
|
+
units
|
15
|
+
end
|
16
|
+
|
17
|
+
def seconds_to_units_in_words(seconds, **options)
|
18
|
+
seconds_to_units(seconds, options).map { |unit, num|
|
19
|
+
"#{num} #{unit}"
|
20
|
+
}.join(', ')
|
21
|
+
end
|
22
|
+
end
|
23
|
+
end
|
data/playground_setup.rb
ADDED
@@ -0,0 +1,30 @@
|
|
1
|
+
ENV['RORY_ENV'] ||= ENV['RACK_ENV'] || 'development'
|
2
|
+
|
3
|
+
require 'bundler/setup'
|
4
|
+
require 'bumbleworks/api'
|
5
|
+
require_relative 'spec/fixtures/bumbleworks_config'
|
6
|
+
require_relative 'spec/support/process_helpers'
|
7
|
+
include ProcessHelpers
|
8
|
+
|
9
|
+
Bumbleworks.start_worker!
|
10
|
+
|
11
|
+
Widget.truncate!
|
12
|
+
WidgetyFidget.truncate!
|
13
|
+
|
14
|
+
widget_processes = 20.times.collect do |i|
|
15
|
+
Widget.new(i).launch_process('task_process')
|
16
|
+
end
|
17
|
+
|
18
|
+
5.times do |i|
|
19
|
+
WidgetyFidget.new(i)
|
20
|
+
end
|
21
|
+
|
22
|
+
wp = Bumbleworks.launch!('waiting_process')
|
23
|
+
|
24
|
+
ProcessHelpers.wait_until(:timeout => 30) do
|
25
|
+
wp.reload.trackers.count == 4
|
26
|
+
end
|
27
|
+
|
28
|
+
widget_processes.first(3).each do |p|
|
29
|
+
p.tasks.map(&:complete)
|
30
|
+
end
|
@@ -0,0 +1,32 @@
|
|
1
|
+
describe Bumbleworks::Api::EntitiesController do
|
2
|
+
describe "#types" do
|
3
|
+
it "returns entity types" do
|
4
|
+
get "/entities"
|
5
|
+
expect(last_response.body).to eq(
|
6
|
+
json_presentation_of(Bumbleworks.entity_classes)
|
7
|
+
)
|
8
|
+
end
|
9
|
+
end
|
10
|
+
|
11
|
+
describe "#index" do
|
12
|
+
it "returns all existing instances of a given entity type" do
|
13
|
+
foo, bar, baz = Widget.new('foo'),
|
14
|
+
Widget.new('bar'),
|
15
|
+
WidgetyFidget.new('baz')
|
16
|
+
get "/entities/widget"
|
17
|
+
expect(last_response.body).to eq(
|
18
|
+
json_presentation_of([foo, bar], :as => 'Entity')
|
19
|
+
)
|
20
|
+
end
|
21
|
+
end
|
22
|
+
|
23
|
+
describe "#show" do
|
24
|
+
it "returns requested instance" do
|
25
|
+
foo = Widget.new(130)
|
26
|
+
get "/entities/widget/130"
|
27
|
+
expect(last_response.body).to eq(
|
28
|
+
json_presentation_of(foo, :as => 'Entity')
|
29
|
+
)
|
30
|
+
end
|
31
|
+
end
|
32
|
+
end
|
@@ -0,0 +1,42 @@
|
|
1
|
+
describe Bumbleworks::Api::ErrorsController do
|
2
|
+
let(:process) { Bumbleworks.launch!('error_process') }
|
3
|
+
let(:error) { Bumbleworks.errors.first }
|
4
|
+
before(:each) do
|
5
|
+
wait_until { process.reload.errors.count > 0 }
|
6
|
+
end
|
7
|
+
|
8
|
+
describe "#index" do
|
9
|
+
it "returns all errors" do
|
10
|
+
Bumbleworks.launch!('error_process')
|
11
|
+
wait_until { Bumbleworks.errors.count == 2 }
|
12
|
+
get "/errors"
|
13
|
+
expect(last_response.body).to eq(
|
14
|
+
json_presentation_of(Bumbleworks.errors, :as => 'Error')
|
15
|
+
)
|
16
|
+
end
|
17
|
+
end
|
18
|
+
|
19
|
+
describe "#show" do
|
20
|
+
it "returns requested error" do
|
21
|
+
get "/processes/#{process.id}/expressions/#{error.expression.expid}/error"
|
22
|
+
expect(last_response.body).to eq(
|
23
|
+
json_presentation_of(error, :as => 'Error')
|
24
|
+
)
|
25
|
+
end
|
26
|
+
end
|
27
|
+
|
28
|
+
describe "#replay" do
|
29
|
+
after(:each) do
|
30
|
+
NaughtyParticipant.naughty_is_ok = false
|
31
|
+
end
|
32
|
+
|
33
|
+
it "attempts to replay error and returns success" do
|
34
|
+
NaughtyParticipant.naughty_is_ok = true
|
35
|
+
put "/processes/#{process.id}/expressions/#{error.expression.expid}/error/replay"
|
36
|
+
wait_until { !process.reload.running? }
|
37
|
+
expect(last_response.body).to eq(
|
38
|
+
{ :status => 'replayed' }.to_json
|
39
|
+
)
|
40
|
+
end
|
41
|
+
end
|
42
|
+
end
|
@@ -0,0 +1,40 @@
|
|
1
|
+
describe Bumbleworks::Api::ExpressionsController do
|
2
|
+
let(:process) { Bumbleworks.launch!('task_process') }
|
3
|
+
let(:expression) { process.expression_at_position('0_0_1') }
|
4
|
+
before(:each) do
|
5
|
+
wait_until { process.reload.tasks.count == 2 }
|
6
|
+
end
|
7
|
+
|
8
|
+
describe "#show" do
|
9
|
+
it "returns requested expression" do
|
10
|
+
get "/processes/#{process.id}/expressions/#{expression.expid}"
|
11
|
+
expect(last_response.body).to eq(
|
12
|
+
json_presentation_of(expression)
|
13
|
+
)
|
14
|
+
end
|
15
|
+
end
|
16
|
+
|
17
|
+
describe "#cancel" do
|
18
|
+
it "attempts to cancel expression and returns success" do
|
19
|
+
allow_any_instance_of(described_class).to receive(:expression).
|
20
|
+
and_return(expression)
|
21
|
+
expect(expression).to receive(:cancel!)
|
22
|
+
delete "/processes/#{process.id}/expressions/#{expression.expid}/cancel"
|
23
|
+
expect(last_response.body).to eq(
|
24
|
+
{ :status => 'cancelled' }.to_json
|
25
|
+
)
|
26
|
+
end
|
27
|
+
end
|
28
|
+
|
29
|
+
describe "#kill" do
|
30
|
+
it "attempts to kill expression and returns success" do
|
31
|
+
allow_any_instance_of(described_class).to receive(:expression).
|
32
|
+
and_return(expression)
|
33
|
+
expect(expression).to receive(:kill!)
|
34
|
+
delete "/processes/#{process.id}/expressions/#{expression.expid}/kill"
|
35
|
+
expect(last_response.body).to eq(
|
36
|
+
{ :status => 'killed' }.to_json
|
37
|
+
)
|
38
|
+
end
|
39
|
+
end
|
40
|
+
end
|
@@ -0,0 +1,50 @@
|
|
1
|
+
describe Bumbleworks::Api::ProcessesController do
|
2
|
+
describe "#index" do
|
3
|
+
before(:each) do
|
4
|
+
allow(Bumbleworks::Api::ProcessPresenter).to receive(:present).
|
5
|
+
with(:processes).
|
6
|
+
and_return(:presented_processes)
|
7
|
+
end
|
8
|
+
|
9
|
+
it "returns requested page with requested number of processes" do
|
10
|
+
allow(Bumbleworks::Process).to receive(:all).
|
11
|
+
with(:limit => 12, :offset => 24).
|
12
|
+
and_return(:processes)
|
13
|
+
get "/processes?page=3&limit=12"
|
14
|
+
expect(last_response.body).to eq(
|
15
|
+
json_presentation_of(:processes, :as => 'Process')
|
16
|
+
)
|
17
|
+
end
|
18
|
+
|
19
|
+
it "defaults to limit 10" do
|
20
|
+
allow(Bumbleworks::Process).to receive(:all).
|
21
|
+
with(:limit => 10, :offset => 10).
|
22
|
+
and_return(:processes)
|
23
|
+
get "/processes?page=2"
|
24
|
+
expect(last_response.body).to eq(
|
25
|
+
json_presentation_of(:processes, :as => 'Process')
|
26
|
+
)
|
27
|
+
end
|
28
|
+
|
29
|
+
it "defaults to page 1" do
|
30
|
+
allow(Bumbleworks::Process).to receive(:all).
|
31
|
+
with(:limit => 3, :offset => 0).
|
32
|
+
and_return(:processes)
|
33
|
+
get "/processes?limit=3"
|
34
|
+
expect(last_response.body).to eq(
|
35
|
+
json_presentation_of(:processes, :as => 'Process')
|
36
|
+
)
|
37
|
+
end
|
38
|
+
end
|
39
|
+
|
40
|
+
describe "#show" do
|
41
|
+
it "returns requested instance" do
|
42
|
+
process = Bumbleworks.launch!('task_process', :entity => Widget.new(41))
|
43
|
+
wait_until { process.tasks.count == 2 }
|
44
|
+
get "/processes/#{process.id}"
|
45
|
+
expect(last_response.body).to eq(
|
46
|
+
json_presentation_of(process)
|
47
|
+
)
|
48
|
+
end
|
49
|
+
end
|
50
|
+
end
|
@@ -0,0 +1,82 @@
|
|
1
|
+
describe Bumbleworks::Api::TasksController do
|
2
|
+
let(:process) { Bumbleworks.launch!('task_process') }
|
3
|
+
before(:each) do
|
4
|
+
wait_until do
|
5
|
+
process.tasks.count == 2
|
6
|
+
end
|
7
|
+
end
|
8
|
+
|
9
|
+
describe "#index" do
|
10
|
+
it "returns all tasks" do
|
11
|
+
tasks = process.tasks.all
|
12
|
+
get "/tasks"
|
13
|
+
expect(last_response.body).to eq(
|
14
|
+
json_presentation_of(tasks)
|
15
|
+
)
|
16
|
+
end
|
17
|
+
end
|
18
|
+
|
19
|
+
describe "#show" do
|
20
|
+
it "returns requested task" do
|
21
|
+
task = process.tasks.first
|
22
|
+
get "/tasks/#{task.id}"
|
23
|
+
expect(last_response.body).to eq(
|
24
|
+
json_presentation_of(task)
|
25
|
+
)
|
26
|
+
end
|
27
|
+
end
|
28
|
+
|
29
|
+
describe "#claim" do
|
30
|
+
it "claims and returns task" do
|
31
|
+
task = process.tasks.first
|
32
|
+
put "/tasks/#{task.id}/claim", :claimant => "horatio"
|
33
|
+
expect(task.reload.claimant).to eq("horatio")
|
34
|
+
expect(last_response.body).to eq(
|
35
|
+
json_presentation_of(task)
|
36
|
+
)
|
37
|
+
end
|
38
|
+
end
|
39
|
+
|
40
|
+
describe "#release" do
|
41
|
+
it "releases and returns task" do
|
42
|
+
task = process.tasks.first
|
43
|
+
task.claim("whiskey")
|
44
|
+
put "/tasks/#{task.id}/release"
|
45
|
+
expect(task.reload.claimant).to be_nil
|
46
|
+
expect(last_response.body).to eq(
|
47
|
+
json_presentation_of(task)
|
48
|
+
)
|
49
|
+
end
|
50
|
+
end
|
51
|
+
|
52
|
+
describe "#complete" do
|
53
|
+
let(:task) { process.tasks.first }
|
54
|
+
before(:each) do
|
55
|
+
allow(Bumbleworks::Task).to receive(:find_by_id).
|
56
|
+
with(task.id).
|
57
|
+
and_return(task)
|
58
|
+
end
|
59
|
+
|
60
|
+
it "completes task and returns success" do
|
61
|
+
expect(task).to receive(:complete).with(:foo => "bar", "foo" => "bar")
|
62
|
+
put "/tasks/#{task.id}/complete", :foo => :bar
|
63
|
+
expect(last_response.body).to eq(
|
64
|
+
{ :status => 'completed' }.to_json
|
65
|
+
)
|
66
|
+
end
|
67
|
+
|
68
|
+
it "returns 422 with error message if task not completable" do
|
69
|
+
allow(task).to receive(:complete).and_raise(
|
70
|
+
Bumbleworks::Task::NotCompletable, "uh oh you can't do that"
|
71
|
+
)
|
72
|
+
put "/tasks/#{task.id}/complete", :foo => :bar
|
73
|
+
expect(last_response.body).to eq(
|
74
|
+
{
|
75
|
+
:status => "not_completable",
|
76
|
+
:message => "uh oh you can't do that"
|
77
|
+
}.to_json
|
78
|
+
)
|
79
|
+
expect(last_response.status).to eq(422)
|
80
|
+
end
|
81
|
+
end
|
82
|
+
end
|