flowthings 0.1.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 +2 -0
- data/.travis.yml +3 -0
- data/Gemfile +9 -0
- data/Guardfile +77 -0
- data/LICENSE +202 -0
- data/README.md +161 -0
- data/Rakefile +2 -0
- data/bin/console +14 -0
- data/bin/setup +7 -0
- data/flowthings.gemspec +31 -0
- data/lib/flowthings/client.rb +63 -0
- data/lib/flowthings/configuration.rb +39 -0
- data/lib/flowthings/connection.rb +23 -0
- data/lib/flowthings/crud/aggregate.rb +18 -0
- data/lib/flowthings/crud/base.rb +41 -0
- data/lib/flowthings/crud/extended_methods.rb +26 -0
- data/lib/flowthings/crud/find.rb +13 -0
- data/lib/flowthings/crud/member_update.rb +14 -0
- data/lib/flowthings/crud/simulate.rb +13 -0
- data/lib/flowthings/error.rb +30 -0
- data/lib/flowthings/platform_objects/api_task.rb +6 -0
- data/lib/flowthings/platform_objects/drop.rb +29 -0
- data/lib/flowthings/platform_objects/flow.rb +12 -0
- data/lib/flowthings/platform_objects/group.rb +10 -0
- data/lib/flowthings/platform_objects/identity.rb +10 -0
- data/lib/flowthings/platform_objects/mqtt.rb +8 -0
- data/lib/flowthings/platform_objects/platform_object_interface.rb +44 -0
- data/lib/flowthings/platform_objects/share.rb +9 -0
- data/lib/flowthings/platform_objects/token.rb +9 -0
- data/lib/flowthings/platform_objects/track.rb +12 -0
- data/lib/flowthings/request.rb +59 -0
- data/lib/flowthings/response/platform_response.rb +16 -0
- data/lib/flowthings/response/raise_errors.rb +42 -0
- data/lib/flowthings/utils/crud_utils.rb +96 -0
- data/lib/flowthings/utils/member.rb +0 -0
- data/lib/flowthings/version.rb +3 -0
- data/lib/flowthings.rb +7 -0
- data/spec/client_configuration_spec.rb +67 -0
- data/spec/client_spec.rb +51 -0
- data/spec/configuration_spec.rb +28 -0
- data/spec/flowthings_spec.rb +7 -0
- data/spec/platform_object_interface_spec.rb +12 -0
- data/spec/platform_objects/api_task_spec.rb +92 -0
- data/spec/platform_objects/drop_spec.rb +180 -0
- data/spec/platform_objects/flow_spec.rb +83 -0
- data/spec/platform_objects/platform_objects_spec.rb +73 -0
- data/spec/platform_objects/track_spec.rb +46 -0
- data/spec/spec_helper.rb +5 -0
- metadata +192 -0
@@ -0,0 +1,41 @@
|
|
1
|
+
require 'flowthings/utils/crud_utils'
|
2
|
+
|
3
|
+
module Flowthings
|
4
|
+
module Crud
|
5
|
+
module Base
|
6
|
+
include Flowthings::CrudUtils
|
7
|
+
|
8
|
+
def create(data, params={})
|
9
|
+
path = mk_path
|
10
|
+
params = mk_params(params)
|
11
|
+
data = mk_data(data)
|
12
|
+
|
13
|
+
platform_post(path, params=params, data=data)
|
14
|
+
end
|
15
|
+
|
16
|
+
def read(id, params={})
|
17
|
+
path = mk_path({id: id})
|
18
|
+
params = mk_params(params)
|
19
|
+
|
20
|
+
platform_get(path, params=params)
|
21
|
+
end
|
22
|
+
|
23
|
+
def update(id, data, params={})
|
24
|
+
path = mk_path(id: id)
|
25
|
+
params = mk_params(params)
|
26
|
+
data = mk_data(data)
|
27
|
+
|
28
|
+
platform_put(path, params=params, data=data)
|
29
|
+
end
|
30
|
+
|
31
|
+
def destroy(id, params={})
|
32
|
+
path = mk_path({id: id})
|
33
|
+
params = mk_params(params)
|
34
|
+
|
35
|
+
platform_delete(path, params=params)
|
36
|
+
end
|
37
|
+
|
38
|
+
alias_method :delete, :destroy
|
39
|
+
end
|
40
|
+
end
|
41
|
+
end
|
@@ -0,0 +1,26 @@
|
|
1
|
+
module Flowthings
|
2
|
+
module Crud
|
3
|
+
module ExtendedMethods
|
4
|
+
def delete_all(params={})
|
5
|
+
end
|
6
|
+
|
7
|
+
def find_many(filters={}, params={})
|
8
|
+
path = mk_path
|
9
|
+
params = mk_params(params)
|
10
|
+
data = []
|
11
|
+
|
12
|
+
@flowIds.each do flowId
|
13
|
+
if filters[flowId]
|
14
|
+
data << {"flowId" => flowId,
|
15
|
+
"params" => filters[flowId]}
|
16
|
+
else
|
17
|
+
data << {"flowId" => flowId}
|
18
|
+
end
|
19
|
+
end
|
20
|
+
|
21
|
+
|
22
|
+
platform_mget(path, params=params, data=data)
|
23
|
+
end
|
24
|
+
end
|
25
|
+
end
|
26
|
+
end
|
@@ -0,0 +1,14 @@
|
|
1
|
+
module Flowthings
|
2
|
+
module Crud
|
3
|
+
module MemberUpdate
|
4
|
+
def member_update(id, member_name, data, params)
|
5
|
+
path = mk_path({tail: member_name,
|
6
|
+
id: id})
|
7
|
+
params = mk_params(params)
|
8
|
+
data = mk_data(data)
|
9
|
+
|
10
|
+
platform_put(path, params=params, data=data)
|
11
|
+
end
|
12
|
+
end
|
13
|
+
end
|
14
|
+
end
|
@@ -0,0 +1,30 @@
|
|
1
|
+
module Flowthings
|
2
|
+
class Error < StandardError
|
3
|
+
def initialize(message="")
|
4
|
+
if message
|
5
|
+
super(message)
|
6
|
+
end
|
7
|
+
end
|
8
|
+
end
|
9
|
+
|
10
|
+
class Error::HttpError < Flowthings::Error
|
11
|
+
attr_reader :http_headers
|
12
|
+
|
13
|
+
def initialize(message, http_headers="")
|
14
|
+
@http_headers = http_headers
|
15
|
+
super(message)
|
16
|
+
end
|
17
|
+
end
|
18
|
+
|
19
|
+
class Error::ObjectError < Flowthings::Error; end
|
20
|
+
|
21
|
+
class Error::ServerError < Error::HttpError; end
|
22
|
+
class Error::ServiceUnavailable < Error::ServerError; end
|
23
|
+
|
24
|
+
class Error::ClientError < Error::HttpError; end
|
25
|
+
class Error::Forbidden < Error::ClientError; end
|
26
|
+
class Error::NotFound < Error::ClientError; end
|
27
|
+
class Error::BadRequest < Error::ClientError; end
|
28
|
+
class Error::RequestTooLarge < Error::ClientError; end
|
29
|
+
|
30
|
+
end
|
@@ -0,0 +1,29 @@
|
|
1
|
+
require 'flowthings/platform_objects/platform_object_interface'
|
2
|
+
require 'flowthings/crud/extended_methods'
|
3
|
+
require 'flowthings/crud/find'
|
4
|
+
require 'flowthings/crud/member_update'
|
5
|
+
require 'flowthings/crud/aggregate'
|
6
|
+
|
7
|
+
module Flowthings
|
8
|
+
|
9
|
+
class Drop < PlatformObjectInterface
|
10
|
+
include Flowthings::Crud::ExtendedMethods
|
11
|
+
include Flowthings::Crud::Find
|
12
|
+
include Flowthings::Crud::MemberUpdate
|
13
|
+
include Flowthings::Crud::Aggregate
|
14
|
+
|
15
|
+
attr_accessor :flowId
|
16
|
+
|
17
|
+
def initialize(flowId, connection, options={})
|
18
|
+
if flowId.kind_of? Array
|
19
|
+
@flowIds = flowId
|
20
|
+
else
|
21
|
+
@flowId = flowId
|
22
|
+
end
|
23
|
+
|
24
|
+
super connection, options
|
25
|
+
end
|
26
|
+
|
27
|
+
end
|
28
|
+
|
29
|
+
end
|
@@ -0,0 +1,12 @@
|
|
1
|
+
require 'flowthings/platform_objects/platform_object_interface'
|
2
|
+
require 'flowthings/crud/find'
|
3
|
+
require 'flowthings/crud/member_update'
|
4
|
+
|
5
|
+
module Flowthings
|
6
|
+
|
7
|
+
class Flow < PlatformObjectInterface
|
8
|
+
include Flowthings::Crud::Find
|
9
|
+
include Flowthings::Crud::MemberUpdate
|
10
|
+
end
|
11
|
+
|
12
|
+
end
|
@@ -0,0 +1,44 @@
|
|
1
|
+
require 'flowthings/request'
|
2
|
+
require 'flowthings/connection'
|
3
|
+
require 'flowthings/crud/base'
|
4
|
+
require 'flowthings/error'
|
5
|
+
|
6
|
+
module Flowthings
|
7
|
+
|
8
|
+
class PlatformObjectInterface
|
9
|
+
|
10
|
+
include Flowthings::Request
|
11
|
+
include Flowthings::Crud::Base
|
12
|
+
|
13
|
+
@path_array = []
|
14
|
+
|
15
|
+
# make a class variable here, a hash,
|
16
|
+
# that holds all the various endpoints in it
|
17
|
+
|
18
|
+
# then make a call here to query the object type
|
19
|
+
# and give it the right endpoint function type.
|
20
|
+
|
21
|
+
def initialize(connection, options={})
|
22
|
+
check_platform_object
|
23
|
+
|
24
|
+
Configuration::VALID_CONFIG_KEYS.each do |key|
|
25
|
+
instance_variable_set("@#{key}", options[key])
|
26
|
+
end
|
27
|
+
|
28
|
+
@connection = connection
|
29
|
+
|
30
|
+
@account_name=options[:account_name]
|
31
|
+
@account_token=options[:account_token]
|
32
|
+
end
|
33
|
+
|
34
|
+
|
35
|
+
private
|
36
|
+
def check_platform_object
|
37
|
+
if self.class == PlatformObjectInterface
|
38
|
+
raise Flowthings::Error::ObjectError, "Use the actual platform objects"
|
39
|
+
end
|
40
|
+
end
|
41
|
+
|
42
|
+
end
|
43
|
+
|
44
|
+
end
|
@@ -0,0 +1,12 @@
|
|
1
|
+
require 'flowthings/platform_objects/platform_object_interface'
|
2
|
+
require 'flowthings/crud/find'
|
3
|
+
require 'flowthings/crud/simulate'
|
4
|
+
|
5
|
+
module Flowthings
|
6
|
+
|
7
|
+
class Track < PlatformObjectInterface
|
8
|
+
include Flowthings::Crud::Find
|
9
|
+
include Flowthings::Crud::Simulate
|
10
|
+
end
|
11
|
+
|
12
|
+
end
|
@@ -0,0 +1,59 @@
|
|
1
|
+
require 'net/http'
|
2
|
+
require 'uri'
|
3
|
+
require 'json'
|
4
|
+
require 'flowthings/response/raise_errors'
|
5
|
+
|
6
|
+
module Flowthings
|
7
|
+
module Request
|
8
|
+
|
9
|
+
include Flowthings::Response::RaiseErrors
|
10
|
+
|
11
|
+
private
|
12
|
+
|
13
|
+
def platform_get(path, params={}, options={})
|
14
|
+
request(:get, path, params, options)
|
15
|
+
end
|
16
|
+
|
17
|
+
def platform_post(path, data, params={}, options={})
|
18
|
+
request(:post, path, params, options, data=data)
|
19
|
+
end
|
20
|
+
|
21
|
+
def platform_put(path, data, params={}, options={})
|
22
|
+
request(:put, path, params, options, data=data)
|
23
|
+
end
|
24
|
+
|
25
|
+
def platform_mget(path, data, params={}, options={})
|
26
|
+
request(:mget, path, params, options, data=data)
|
27
|
+
end
|
28
|
+
|
29
|
+
def platform_delete(path, params={}, options={})
|
30
|
+
request(:delete, path, params, options)
|
31
|
+
end
|
32
|
+
|
33
|
+
def request(method, path, params, options, data={})
|
34
|
+
|
35
|
+
case method.to_sym
|
36
|
+
when :get, :delete
|
37
|
+
response = @connection.request(path: path,
|
38
|
+
query: params,
|
39
|
+
method: method.to_sym)
|
40
|
+
when :post, :put, :mget
|
41
|
+
body = params unless params.empty?
|
42
|
+
body = JSON.generate(body)
|
43
|
+
response = @connection.request(path: path,
|
44
|
+
query: params,
|
45
|
+
method: method.to_sym,
|
46
|
+
body: body)
|
47
|
+
end
|
48
|
+
|
49
|
+
raise_error(response)
|
50
|
+
|
51
|
+
|
52
|
+
response = response.data
|
53
|
+
|
54
|
+
response = JSON.parse response[:body]
|
55
|
+
response = response["body"]
|
56
|
+
end
|
57
|
+
|
58
|
+
end
|
59
|
+
end
|
@@ -0,0 +1,16 @@
|
|
1
|
+
module Flowthings
|
2
|
+
class PlatformResponse
|
3
|
+
attr_reader :head, :body, :full_message, :errors, :status, :response_headers, :excon_object
|
4
|
+
|
5
|
+
def initialize(response)
|
6
|
+
@excon_object = response
|
7
|
+
@status = response.status
|
8
|
+
@response_headers = response.headers
|
9
|
+
@full_message = response.body
|
10
|
+
@head = @full_message["head"]
|
11
|
+
@body = @full_message["body"]
|
12
|
+
@errors = @head["errors"]
|
13
|
+
end
|
14
|
+
|
15
|
+
end
|
16
|
+
end
|
@@ -0,0 +1,42 @@
|
|
1
|
+
require 'json'
|
2
|
+
|
3
|
+
module Flowthings
|
4
|
+
module Response
|
5
|
+
module RaiseErrors
|
6
|
+
|
7
|
+
private
|
8
|
+
def raise_error(response)
|
9
|
+
|
10
|
+
begin
|
11
|
+
body = JSON.parse response[:body]
|
12
|
+
rescue JSON::ParserError => e
|
13
|
+
body = response[:body]
|
14
|
+
end
|
15
|
+
|
16
|
+
status = response[:status].to_i
|
17
|
+
|
18
|
+
head = body["head"]
|
19
|
+
errors = head["errors"]
|
20
|
+
|
21
|
+
|
22
|
+
case status
|
23
|
+
when 400
|
24
|
+
raise Flowthings::Error::BadRequest.new errors, head
|
25
|
+
when 403
|
26
|
+
raise Flowthings::Error::NotFound.new errors, head
|
27
|
+
when 404
|
28
|
+
raise Flowthings::Error::NotFound.new errors, head
|
29
|
+
when 413
|
30
|
+
raise Flowthings::Error::Forbidden.new errors, head
|
31
|
+
when 500
|
32
|
+
raise Flowthings::Error::ServerError.new errors, head
|
33
|
+
when 503
|
34
|
+
raise Flowthings::Error::ServiceUnavailable.new "503 no service is available to handle this request", head
|
35
|
+
end
|
36
|
+
|
37
|
+
end
|
38
|
+
|
39
|
+
|
40
|
+
end
|
41
|
+
end
|
42
|
+
end
|
@@ -0,0 +1,96 @@
|
|
1
|
+
module Flowthings
|
2
|
+
module CrudUtils
|
3
|
+
private
|
4
|
+
def mk_data(data, params = {})
|
5
|
+
data
|
6
|
+
end
|
7
|
+
|
8
|
+
def mk_path(params = {})
|
9
|
+
service_path = {
|
10
|
+
"Flowthings::Drop" => "drop",
|
11
|
+
"Flowthings::Flow" => "flow",
|
12
|
+
"Flowthings::Track" => "track",
|
13
|
+
"Flowthings::Identity" => "identity",
|
14
|
+
"Flowthings::Token" => "token",
|
15
|
+
"Flowthings::Share" => "share",
|
16
|
+
"Flowthings::Group" => "group",
|
17
|
+
"Flowthings::ApiTask" => "api-task",
|
18
|
+
"Flowthings::Mqtt" => "mqtt"
|
19
|
+
}
|
20
|
+
|
21
|
+
path = [@platform_version, @account_name, service_path[self.class.name]]
|
22
|
+
|
23
|
+
if @flowId
|
24
|
+
path << @flowId
|
25
|
+
end
|
26
|
+
|
27
|
+
if params[:id]
|
28
|
+
path << params[:id]
|
29
|
+
end
|
30
|
+
|
31
|
+
# a tail is somethings we put after the id on the flow.
|
32
|
+
# "/aggregate" would be an example
|
33
|
+
if params[:tail]
|
34
|
+
path << params[:tail]
|
35
|
+
end
|
36
|
+
|
37
|
+
path = "/" + path.join('/')
|
38
|
+
end
|
39
|
+
|
40
|
+
def mk_regex()
|
41
|
+
|
42
|
+
end
|
43
|
+
|
44
|
+
def mk_params(params = {})
|
45
|
+
|
46
|
+
made_params = {}
|
47
|
+
|
48
|
+
if params[:start]
|
49
|
+
made_params[:start] = params[:start]
|
50
|
+
end
|
51
|
+
|
52
|
+
if params[:limit]
|
53
|
+
made_params[:limit] = params[:limit]
|
54
|
+
end
|
55
|
+
|
56
|
+
if params[:sort]
|
57
|
+
made_params[:sort] = params[:sort]
|
58
|
+
end
|
59
|
+
|
60
|
+
if params[:order]
|
61
|
+
made_params[:order] = params[:order]
|
62
|
+
end
|
63
|
+
|
64
|
+
if params[:only]
|
65
|
+
made_params[:only] = params[:only]
|
66
|
+
end
|
67
|
+
|
68
|
+
if params[:refs]
|
69
|
+
made_params[:refs] = 1
|
70
|
+
end
|
71
|
+
|
72
|
+
if params[:hints] == false
|
73
|
+
made_params[:hints] = false
|
74
|
+
end
|
75
|
+
|
76
|
+
if params[:filter]
|
77
|
+
#we'll make this more dynamic later
|
78
|
+
made_params[:filter] = params[:filter]
|
79
|
+
end
|
80
|
+
|
81
|
+
made_params
|
82
|
+
end
|
83
|
+
|
84
|
+
def mk_filter(params = {})
|
85
|
+
# this is just a string for now
|
86
|
+
filter = []
|
87
|
+
|
88
|
+
params.each do |key, value|
|
89
|
+
|
90
|
+
end
|
91
|
+
|
92
|
+
filter
|
93
|
+
end
|
94
|
+
|
95
|
+
end
|
96
|
+
end
|
File without changes
|
data/lib/flowthings.rb
ADDED
@@ -0,0 +1,67 @@
|
|
1
|
+
require 'spec_helper'
|
2
|
+
|
3
|
+
describe Flowthings::Client do
|
4
|
+
before do
|
5
|
+
@keys = Flowthings::Configuration::VALID_CONFIG_KEYS
|
6
|
+
end
|
7
|
+
|
8
|
+
describe 'configuration' do
|
9
|
+
|
10
|
+
describe 'with module configuration' do
|
11
|
+
before do
|
12
|
+
Flowthings.configure do |config|
|
13
|
+
@keys.each do |key|
|
14
|
+
config.send("#{key}=", key)
|
15
|
+
end
|
16
|
+
end
|
17
|
+
end
|
18
|
+
|
19
|
+
after do
|
20
|
+
Flowthings.reset
|
21
|
+
end
|
22
|
+
|
23
|
+
it 'should inherit module configuration' do
|
24
|
+
api = Flowthings::Client.new
|
25
|
+
|
26
|
+
@keys.each do |key|
|
27
|
+
expect(api.send(key)).to eq(key)
|
28
|
+
end
|
29
|
+
end
|
30
|
+
end
|
31
|
+
|
32
|
+
describe 'with class configuration' do
|
33
|
+
before do
|
34
|
+
@config = {
|
35
|
+
account_name: 'aa',
|
36
|
+
account_token: 'bb',
|
37
|
+
endpoint: 'dd',
|
38
|
+
user_agent: 'ee',
|
39
|
+
secure: 'ff',
|
40
|
+
platform_version: 'gg',
|
41
|
+
}
|
42
|
+
end
|
43
|
+
|
44
|
+
it 'should override module configuration' do
|
45
|
+
api = Flowthings::Client.new(@config)
|
46
|
+
|
47
|
+
@keys.each do |key|
|
48
|
+
expect(api.send(key)).to eq(@config[key])
|
49
|
+
end
|
50
|
+
end
|
51
|
+
|
52
|
+
it 'should override module configuration after' do
|
53
|
+
api = Flowthings::Client.new
|
54
|
+
|
55
|
+
@config.each do |key, value|
|
56
|
+
api.send("#{key}=", value)
|
57
|
+
end
|
58
|
+
|
59
|
+
@keys.each do |key|
|
60
|
+
expect(api.send("#{key}")).to eq(@config[key])
|
61
|
+
end
|
62
|
+
end
|
63
|
+
|
64
|
+
end
|
65
|
+
end
|
66
|
+
|
67
|
+
end
|
data/spec/client_spec.rb
ADDED
@@ -0,0 +1,51 @@
|
|
1
|
+
require 'spec_helper'
|
2
|
+
require 'active_support/inflector'
|
3
|
+
|
4
|
+
describe Flowthings::Client do
|
5
|
+
services = ["api_task", "drop", "flow", "group", "identity", "mqtt", "share", "token", "track"]
|
6
|
+
|
7
|
+
describe 'interfaces' do
|
8
|
+
before do
|
9
|
+
@api = Flowthings::Client.new
|
10
|
+
end
|
11
|
+
|
12
|
+
services.each do |service|
|
13
|
+
service_class = "flowthings/" + service
|
14
|
+
service_class = service_class.camelize.constantize
|
15
|
+
|
16
|
+
describe ".#{service}" do
|
17
|
+
it "the method should exist" do
|
18
|
+
expect(@api.respond_to? "#{service}").to be true
|
19
|
+
end
|
20
|
+
|
21
|
+
it "the method should construct the proper class" do
|
22
|
+
if service == 'drop'
|
23
|
+
initialized_service = @api.send("#{service}", "d")
|
24
|
+
expect(initialized_service.class).to eq(service_class)
|
25
|
+
else
|
26
|
+
initialized_service = @api.send("#{service}")
|
27
|
+
expect(initialized_service.class).to eq(service_class)
|
28
|
+
end
|
29
|
+
end
|
30
|
+
|
31
|
+
if service == 'drop'
|
32
|
+
it "should throw an error if it is called without an argument" do
|
33
|
+
expect { @api.send("#{service}") }.to raise_error
|
34
|
+
end
|
35
|
+
|
36
|
+
it "should raise an argument error" do
|
37
|
+
expect { @api.send("#{service}") }.to raise_error(ArgumentError)
|
38
|
+
end
|
39
|
+
|
40
|
+
it "should not throw an error if it is called with an argument" do
|
41
|
+
expect { @api.send("#{service}", "d") }.not_to raise_error
|
42
|
+
end
|
43
|
+
end
|
44
|
+
|
45
|
+
end # describe block
|
46
|
+
|
47
|
+
end # services loop
|
48
|
+
|
49
|
+
end #interfaces
|
50
|
+
|
51
|
+
end
|
@@ -0,0 +1,28 @@
|
|
1
|
+
require 'spec_helper'
|
2
|
+
|
3
|
+
describe 'configuration' do
|
4
|
+
|
5
|
+
Flowthings::Configuration::VALID_CONFIG_KEYS.each do |key|
|
6
|
+
describe ".#{key}" do
|
7
|
+
it 'should return the default value' do
|
8
|
+
expect(Flowthings.send(key)).to eq(Flowthings::Configuration.const_get("DEFAULT_#{key.upcase}"))
|
9
|
+
end
|
10
|
+
end
|
11
|
+
end
|
12
|
+
|
13
|
+
after do
|
14
|
+
Flowthings.reset
|
15
|
+
end
|
16
|
+
|
17
|
+
describe '.configure' do
|
18
|
+
Flowthings::Configuration::VALID_CONFIG_KEYS.each do |key|
|
19
|
+
it "should set the #{key}" do
|
20
|
+
Flowthings.configure do |config|
|
21
|
+
config.send("#{key}=", key)
|
22
|
+
expect(Flowthings.send(key)).to eq(key)
|
23
|
+
end
|
24
|
+
end
|
25
|
+
end
|
26
|
+
end
|
27
|
+
|
28
|
+
end
|