cts-mpx 1.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 +50 -0
- data/.rspec +2 -0
- data/.rubocop.yml +88 -0
- data/CONTRIBUTING +8 -0
- data/COPYRIGHT +10 -0
- data/EXAMPLES.md +81 -0
- data/Gemfile +4 -0
- data/Gemfile.lock +172 -0
- data/Guardfile +41 -0
- data/LICENSE +201 -0
- data/NOTICE +9 -0
- data/README.md +60 -0
- data/Rakefile +6 -0
- data/bin/console +14 -0
- data/bin/setup +8 -0
- data/config/data_services.json +423 -0
- data/config/ingest_services.json +14 -0
- data/config/root_registry_sea1.json +118 -0
- data/config/web_services.json +544 -0
- data/cts-mpx.gemspec +43 -0
- data/examples/basic_query.rb +23 -0
- data/examples/login.rb +7 -0
- data/examples/update_media.rb +16 -0
- data/examples/update_procedurally.rb +20 -0
- data/lib/cts/mpx.rb +42 -0
- data/lib/cts/mpx/driver.rb +47 -0
- data/lib/cts/mpx/driver/assemblers.rb +96 -0
- data/lib/cts/mpx/driver/connections.rb +41 -0
- data/lib/cts/mpx/driver/exceptions.rb +50 -0
- data/lib/cts/mpx/driver/helpers.rb +67 -0
- data/lib/cts/mpx/driver/page.rb +38 -0
- data/lib/cts/mpx/driver/request.rb +60 -0
- data/lib/cts/mpx/driver/response.rb +72 -0
- data/lib/cts/mpx/entries.rb +80 -0
- data/lib/cts/mpx/entry.rb +100 -0
- data/lib/cts/mpx/field.rb +38 -0
- data/lib/cts/mpx/fields.rb +120 -0
- data/lib/cts/mpx/query.rb +115 -0
- data/lib/cts/mpx/registry.rb +60 -0
- data/lib/cts/mpx/service.rb +70 -0
- data/lib/cts/mpx/services.rb +113 -0
- data/lib/cts/mpx/services/data.rb +124 -0
- data/lib/cts/mpx/services/ingest.rb +60 -0
- data/lib/cts/mpx/services/web.rb +90 -0
- data/lib/cts/mpx/user.rb +74 -0
- data/lib/cts/mpx/validators.rb +51 -0
- data/lib/cts/mpx/version.rb +6 -0
- data/sdk-ring-diagram.png +0 -0
- data/sdk-uml.png +0 -0
- data/uml.nomnoml +242 -0
- metadata +401 -0
data/cts-mpx.gemspec
ADDED
@@ -0,0 +1,43 @@
|
|
1
|
+
lib = File.expand_path('lib', __dir__)
|
2
|
+
$LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
|
3
|
+
require 'cts/mpx/version'
|
4
|
+
|
5
|
+
Gem::Specification.new do |spec|
|
6
|
+
spec.name = "cts-mpx"
|
7
|
+
spec.version = Cts::Mpx::VERSION
|
8
|
+
spec.authors = ["Ernie Brodeur"]
|
9
|
+
spec.email = ["ernest.brodeur@cable.comcast.net"]
|
10
|
+
|
11
|
+
spec.summary = "Ruby bindings for MPX services."
|
12
|
+
spec.description = "."
|
13
|
+
|
14
|
+
spec.files = `git ls-files -z`.split("\x0").reject { |f| f.match(%r{^(test|spec|features)/}) }
|
15
|
+
spec.bindir = "exe"
|
16
|
+
spec.executables = spec.files.grep(%r{^exe/}) { |f| File.basename(f) }
|
17
|
+
spec.require_paths = ["lib"]
|
18
|
+
spec.required_ruby_version = '>= 2.4.0'
|
19
|
+
|
20
|
+
spec.add_runtime_dependency "creatable", "1.0.1"
|
21
|
+
spec.add_runtime_dependency "excon"
|
22
|
+
spec.add_runtime_dependency "oj", "3.5.0"
|
23
|
+
|
24
|
+
spec.add_development_dependency 'bump'
|
25
|
+
spec.add_development_dependency "bundler"
|
26
|
+
spec.add_development_dependency "erubis"
|
27
|
+
spec.add_development_dependency "gli"
|
28
|
+
spec.add_development_dependency "guard"
|
29
|
+
spec.add_development_dependency "guard-bundler"
|
30
|
+
spec.add_development_dependency "guard-rspec"
|
31
|
+
spec.add_development_dependency "guard-rubocop"
|
32
|
+
spec.add_development_dependency "guard-yard"
|
33
|
+
spec.add_development_dependency "pry"
|
34
|
+
spec.add_development_dependency 'pry-rescue'
|
35
|
+
spec.add_development_dependency 'pry-stack_explorer'
|
36
|
+
spec.add_development_dependency "rake"
|
37
|
+
spec.add_development_dependency "rspec"
|
38
|
+
spec.add_development_dependency "rubocop", "~>0.52.1"
|
39
|
+
spec.add_development_dependency "rubocop-rspec"
|
40
|
+
spec.add_development_dependency "simplecov"
|
41
|
+
spec.add_development_dependency "simplecov-console"
|
42
|
+
spec.add_development_dependency "yard"
|
43
|
+
end
|
@@ -0,0 +1,23 @@
|
|
1
|
+
require 'cts/mpx'
|
2
|
+
|
3
|
+
user = Cts::Mpx::User.create(username: 'username', password: '*****').sign_in
|
4
|
+
account = "http://access.auth.theplatform.com/data/Account/0000000000"
|
5
|
+
service = 'Media Data Service'
|
6
|
+
endpoint = 'Media'
|
7
|
+
|
8
|
+
# build our query, and immediately run it.
|
9
|
+
q = Query.create(account: account, service: service, endpoint: endpoint, fields: 'id,guid,title').run user: user
|
10
|
+
|
11
|
+
### Print out a single entry
|
12
|
+
puts q.entries.first.to_h
|
13
|
+
|
14
|
+
### Print out just the intries
|
15
|
+
puts q.entries.to_h
|
16
|
+
|
17
|
+
### Print out the query (including entries)
|
18
|
+
puts q.entries.to_h
|
19
|
+
|
20
|
+
### Print out the query (without entries)
|
21
|
+
puts q.entries.to_h include_entries: false
|
22
|
+
|
23
|
+
user.sign_out
|
data/examples/login.rb
ADDED
@@ -0,0 +1,16 @@
|
|
1
|
+
require 'cts/mpx'
|
2
|
+
|
3
|
+
user = Cts::Mpx::User.create(username: 'username', password: '*****').sign_in
|
4
|
+
account = "http://access.auth.theplatform.com/data/Account/0000000000"
|
5
|
+
service = 'Media Data Service'
|
6
|
+
endpoint = 'Media'
|
7
|
+
|
8
|
+
# build our query, and immediately run it.
|
9
|
+
q = Query.create(account: account, service: service, endpoint: endpoint, fields: 'id,guid,title').run user: user
|
10
|
+
|
11
|
+
q.entries.each do |entry|
|
12
|
+
entry.title += '!!!'
|
13
|
+
entry.save
|
14
|
+
end
|
15
|
+
|
16
|
+
user.sign_out
|
@@ -0,0 +1,20 @@
|
|
1
|
+
require 'cts/mpx'
|
2
|
+
|
3
|
+
user = Cts::Mpx::User.create(username: 'username', password: '*****').sign_in
|
4
|
+
account = "http://access.auth.theplatform.com/data/Account/0000000000"
|
5
|
+
service = 'Media Data Service'
|
6
|
+
endpoint = 'Media'
|
7
|
+
|
8
|
+
response = Cts::Mpx::Services::Data.get user: user, service: service, endpoint: endpoint, account: account, fields: 'id,guid,title,description'
|
9
|
+
media = response.page
|
10
|
+
|
11
|
+
# modify them all
|
12
|
+
media.entries.each { |e| e["description"] = '1' }
|
13
|
+
|
14
|
+
# put them back
|
15
|
+
response = Cts::Mpx::Services::Data.put user: user, service: 'Media Data Service', endpoint: endpoint, account: account, page: media
|
16
|
+
|
17
|
+
# show response
|
18
|
+
puts response.status
|
19
|
+
|
20
|
+
user.sign_out
|
data/lib/cts/mpx.rb
ADDED
@@ -0,0 +1,42 @@
|
|
1
|
+
require 'base64'
|
2
|
+
require 'creatable'
|
3
|
+
require 'excon'
|
4
|
+
require 'oj'
|
5
|
+
require 'uri'
|
6
|
+
require 'cts/mpx/version'
|
7
|
+
|
8
|
+
# ring 1 (driver)
|
9
|
+
require 'cts/mpx/driver'
|
10
|
+
require 'cts/mpx/driver/assemblers'
|
11
|
+
require 'cts/mpx/driver/connections'
|
12
|
+
require 'cts/mpx/driver/exceptions'
|
13
|
+
require 'cts/mpx/driver/helpers'
|
14
|
+
require 'cts/mpx/driver/page'
|
15
|
+
require 'cts/mpx/driver/response'
|
16
|
+
require 'cts/mpx/driver/request'
|
17
|
+
|
18
|
+
# ring 2 (depends on driver)
|
19
|
+
require 'cts/mpx/validators'
|
20
|
+
require 'cts/mpx/user'
|
21
|
+
require 'cts/mpx/registry'
|
22
|
+
require 'cts/mpx/service'
|
23
|
+
require 'cts/mpx/services'
|
24
|
+
require 'cts/mpx/services/data'
|
25
|
+
require 'cts/mpx/services/web'
|
26
|
+
require 'cts/mpx/services/ingest'
|
27
|
+
|
28
|
+
# ring 3 (depends on ring 2 services)
|
29
|
+
require 'cts/mpx/field'
|
30
|
+
require 'cts/mpx/fields'
|
31
|
+
require 'cts/mpx/entry'
|
32
|
+
require 'cts/mpx/entries'
|
33
|
+
require 'cts/mpx/query'
|
34
|
+
|
35
|
+
# Comcast Technical Solutions
|
36
|
+
module Cts
|
37
|
+
# Media Platform
|
38
|
+
module Mpx
|
39
|
+
Services.initialize
|
40
|
+
Registry.initialize
|
41
|
+
end
|
42
|
+
end
|
@@ -0,0 +1,47 @@
|
|
1
|
+
module Cts
|
2
|
+
module Mpx
|
3
|
+
# responsible for low level calls to MPX
|
4
|
+
module Driver
|
5
|
+
module_function
|
6
|
+
|
7
|
+
#
|
8
|
+
# path to our gem directory, includes support for bundled env's.
|
9
|
+
#
|
10
|
+
# @return [String] full path to the root of our gem directory.
|
11
|
+
#
|
12
|
+
def gem_dir
|
13
|
+
return Dir.pwd unless Gem.loaded_specs.include? 'cts-mpx'
|
14
|
+
Gem.loaded_specs['cts-mpx'].full_gem_path
|
15
|
+
end
|
16
|
+
|
17
|
+
#
|
18
|
+
# path to our config files
|
19
|
+
#
|
20
|
+
# @return [String] full path to the root of our gem directory.
|
21
|
+
#
|
22
|
+
def config_dir
|
23
|
+
"#{gem_dir}/config"
|
24
|
+
end
|
25
|
+
|
26
|
+
#
|
27
|
+
# load a json file into a simple hash
|
28
|
+
#
|
29
|
+
# @param [String] filename filename to load
|
30
|
+
#
|
31
|
+
# @raise [RuntimeError] if the filename does not exist.
|
32
|
+
# @raise [RuntimeError] if the file cannot be parsed, supplies the exception.
|
33
|
+
#
|
34
|
+
# @return [Hash] data from the file
|
35
|
+
#
|
36
|
+
def load_json_file(filename)
|
37
|
+
raise "#{filename} does not exist" unless File.exist? filename
|
38
|
+
|
39
|
+
begin
|
40
|
+
Oj.load File.read filename
|
41
|
+
rescue Oj::ParseError => exception
|
42
|
+
raise "#{filename}: #{exception.message}"
|
43
|
+
end
|
44
|
+
end
|
45
|
+
end
|
46
|
+
end
|
47
|
+
end
|
@@ -0,0 +1,96 @@
|
|
1
|
+
module Cts
|
2
|
+
module Mpx
|
3
|
+
module Driver
|
4
|
+
#
|
5
|
+
# collection of methods used to assemble various parts of a request.
|
6
|
+
#
|
7
|
+
module Assemblers
|
8
|
+
module_function
|
9
|
+
|
10
|
+
# assembles user service and account_id into a host string
|
11
|
+
# @param [Cts::Mpx::User] user user to make calls with
|
12
|
+
# @param [String] service title of a service
|
13
|
+
# @param [String] account_id long form account_id id (ownerId)
|
14
|
+
# @raise [ArgumentError] if user or service is not supplied
|
15
|
+
# @raise [RuntimeError] if the user token is not set
|
16
|
+
# @return [String] assembled scheme and host
|
17
|
+
def host(user: nil, service: nil, account_id: 'urn:theplatform:auth:root')
|
18
|
+
Helpers.required_arguments %i[user service], binding
|
19
|
+
user.token!
|
20
|
+
|
21
|
+
service = Services[service]
|
22
|
+
u = URI.parse service.url
|
23
|
+
|
24
|
+
[u.scheme, u.host].join('://')
|
25
|
+
end
|
26
|
+
|
27
|
+
# Assembles service, endpoint, extra_path, ids, and account_id into a host path
|
28
|
+
# @param [String] service title of a service
|
29
|
+
# @param [String] endpoint endpoint to make the call against
|
30
|
+
# @param [String] extra_path additional part to add to the path
|
31
|
+
# @param [String] ids comma delimited list of short id's to add to the path.
|
32
|
+
# @param [String] account_id long form account_id id (ownerId)
|
33
|
+
# @raise [ArgumentError] if service or endpoint is not supplied
|
34
|
+
# @return [String] assembled path for a data call
|
35
|
+
def path(service: nil, endpoint: nil, extra_path: nil, ids: nil, account_id: 'urn:theplatform:auth:root')
|
36
|
+
Helpers.required_arguments %i[service endpoint], binding
|
37
|
+
service = Services[service]
|
38
|
+
|
39
|
+
path = "#{URI.parse(service.url(account_id)).path}/#{service.path}/#{endpoint}"
|
40
|
+
path += "/#{extra_path}" if extra_path
|
41
|
+
path += "/feed" if service.type == 'data'
|
42
|
+
path += "/#{ids}" if ids
|
43
|
+
path
|
44
|
+
end
|
45
|
+
|
46
|
+
# Assembles service, endpoint, query, range, count, entries, sort and account_id into a query
|
47
|
+
# @param [Cts::Mpx::User] user user to make calls with
|
48
|
+
# @param [String] account_id long form account_id id (ownerId)
|
49
|
+
# @param [String] service title of a service
|
50
|
+
# @param [String] endpoint endpoint to make the call against
|
51
|
+
# @param [Hash] query any additional parameters to add
|
52
|
+
# @param [String] range string (service) format of a range.
|
53
|
+
# @param [TrueFalse] count ask for a count of objects from the services.
|
54
|
+
# @param [TrueFalse] entries return an array of entries.
|
55
|
+
# @param [String] sort set the sort field
|
56
|
+
# @raise [ArgumentError] if user, service or endpoint is not supplied
|
57
|
+
# @raise [RuntimeError] if the user token is not set
|
58
|
+
# @return [Hash] assembled query for a data call
|
59
|
+
def query(user: nil, account: nil, service: nil, endpoint: nil, query: {}, range: nil, count: nil, entries: nil, sort: nil)
|
60
|
+
Helpers.required_arguments %i[user service endpoint], binding
|
61
|
+
user.token!
|
62
|
+
|
63
|
+
service = Services[service]
|
64
|
+
|
65
|
+
h = {}
|
66
|
+
if service.type == 'data'
|
67
|
+
h.merge!(token: user.token, schema: service.schema, form: service.form)
|
68
|
+
h.merge!(query_data(range: range, count: count, entries: entries, sort: sort))
|
69
|
+
else
|
70
|
+
h.merge!(token: user.token, schema: service.endpoints[endpoint]['schema'], form: service.form)
|
71
|
+
end
|
72
|
+
|
73
|
+
h[:account] = account if account
|
74
|
+
h.delete :token if user.token == 'sign_in_token'
|
75
|
+
h.merge! query
|
76
|
+
h
|
77
|
+
end
|
78
|
+
|
79
|
+
# Assembles range, count, entries, sort into a query
|
80
|
+
# @param [String] range string (service) format of a range.
|
81
|
+
# @param [TrueFalse] count ask for a count of objects from the services.
|
82
|
+
# @param [TrueFalse] entries return an array of entries.
|
83
|
+
# @param [String] sort set the sort field
|
84
|
+
# @return [Hash] assembled query for a data call
|
85
|
+
def query_data(range: nil, count: nil, entries: nil, sort: nil)
|
86
|
+
h = {}
|
87
|
+
h.store :range, range if range
|
88
|
+
h.store :count, count if count
|
89
|
+
h.store :entries, entries if entries
|
90
|
+
h.store :sort, sort if sort
|
91
|
+
h
|
92
|
+
end
|
93
|
+
end
|
94
|
+
end
|
95
|
+
end
|
96
|
+
end
|
@@ -0,0 +1,41 @@
|
|
1
|
+
module Cts
|
2
|
+
module Mpx
|
3
|
+
module Driver
|
4
|
+
#
|
5
|
+
# Container for active connections to the data service.
|
6
|
+
#
|
7
|
+
module Connections
|
8
|
+
module_function
|
9
|
+
|
10
|
+
#
|
11
|
+
# Addressable method for active connections. If you provide a string that is not active, an active one
|
12
|
+
# will be created.
|
13
|
+
#
|
14
|
+
# @param [String] uri uri of a service to connect to, must contain theplatform.
|
15
|
+
#
|
16
|
+
# @return [Excon] assembled excon objects with service defaults.
|
17
|
+
# @return [Excon[]] if nil, an array of all open connections.
|
18
|
+
#
|
19
|
+
def [](uri = nil)
|
20
|
+
return @open_connections unless uri
|
21
|
+
begin
|
22
|
+
parsed_uri = URI.parse uri
|
23
|
+
rescue URI::InvalidURIError
|
24
|
+
raise ArgumentError, "#{uri} is not a uri"
|
25
|
+
end
|
26
|
+
|
27
|
+
raise ArgumentError, "#{uri} does not contain theplatform in it." unless parsed_uri.host.include? "theplatform"
|
28
|
+
|
29
|
+
Excon.new([parsed_uri.scheme, parsed_uri.host].join("://"), persistent: true) unless @open_connections.include? parsed_uri.host
|
30
|
+
end
|
31
|
+
|
32
|
+
Excon.defaults[:headers] = {
|
33
|
+
'Content-Type' => "application/json",
|
34
|
+
"User-Agent" => "cts-mpx ruby sdk version #{Cts::Mpx::VERSION}",
|
35
|
+
'Content-Encoding' => 'bzip2,xz,gzip,deflate'
|
36
|
+
}
|
37
|
+
@open_connections = []
|
38
|
+
end
|
39
|
+
end
|
40
|
+
end
|
41
|
+
end
|
@@ -0,0 +1,50 @@
|
|
1
|
+
module Cts
|
2
|
+
module Mpx
|
3
|
+
module Driver
|
4
|
+
module Exceptions
|
5
|
+
module_function
|
6
|
+
|
7
|
+
# Raise an ArgumentError if the argument does not pass Validators.account_id?
|
8
|
+
# @param [Object] argument argument to test if it is a valid account_id
|
9
|
+
# @raise [ArgumentError] if the argument is not a valid account_id
|
10
|
+
# @return [nil]
|
11
|
+
def raise_unless_account_id(argument)
|
12
|
+
raise ArgumentError, "#{argument} is not a valid account_id" unless Validators.account_id? argument
|
13
|
+
nil
|
14
|
+
end
|
15
|
+
|
16
|
+
# Raise an ArgumentError if the argument is not of the supplied type
|
17
|
+
# @param [Object] data argument to test if it is the correct type
|
18
|
+
# @param [Object] type type to test for
|
19
|
+
# @raise [ArgumentError] if the argument is not of the correct type
|
20
|
+
# @return [nil]
|
21
|
+
def raise_unless_argument_error?(data, type = nil, &block)
|
22
|
+
msg = "#{data} is not a valid #{type}"
|
23
|
+
if block
|
24
|
+
raise ArgumentError, msg unless Validators.argument_error?(data, type, &block)
|
25
|
+
elsif Validators.argument_error?(data, type, &block)
|
26
|
+
raise ArgumentError, msg
|
27
|
+
end
|
28
|
+
nil
|
29
|
+
end
|
30
|
+
|
31
|
+
# Raise an ArgumentError if the argument does not pass Validators.reference?
|
32
|
+
# @param [Object] argument argument to test if it is a valid reference
|
33
|
+
# @raise [ArgumentError] if the argument is not a valid reference
|
34
|
+
# @return [nil]
|
35
|
+
def raise_unless_reference?(argument)
|
36
|
+
raise ArgumentError, "#{argument} is not a valid reference" unless Validators.reference? argument
|
37
|
+
nil
|
38
|
+
end
|
39
|
+
|
40
|
+
# Raise an ArgumentError if the keyword is not supplied.
|
41
|
+
# @param [Object] keyword keyword to assure is supplied
|
42
|
+
# @raise [ArgumentError] if the keyword is not suppplied
|
43
|
+
# @return [nil]
|
44
|
+
def raise_unless_required_keyword?(keyword: nil)
|
45
|
+
raise ArgumentError, "#{keyword} is a required keyword." unless keyword && keyword
|
46
|
+
end
|
47
|
+
end
|
48
|
+
end
|
49
|
+
end
|
50
|
+
end
|
@@ -0,0 +1,67 @@
|
|
1
|
+
module Cts
|
2
|
+
module Mpx
|
3
|
+
module Driver
|
4
|
+
#
|
5
|
+
# Collection of simple helpers for the development of the SDK
|
6
|
+
#
|
7
|
+
module Helpers
|
8
|
+
module_function
|
9
|
+
|
10
|
+
#
|
11
|
+
# used to raise an exception if the array of objects is not of the specified type.
|
12
|
+
#
|
13
|
+
# @param [Object[]] objects array of objects to itterate through
|
14
|
+
# @param [Class] type class to check the object array against.
|
15
|
+
#
|
16
|
+
# @raise [ArgumentError] if the argument is not of the specified type
|
17
|
+
#
|
18
|
+
# @return [nil] nil
|
19
|
+
#
|
20
|
+
def raise_if_not_a(objects, type)
|
21
|
+
objects.each { |k| Exceptions.raise_unless_argument_error?(k, type) }
|
22
|
+
nil
|
23
|
+
end
|
24
|
+
|
25
|
+
#
|
26
|
+
# Raise an error if any object in the array is not of a type Array
|
27
|
+
#
|
28
|
+
# @param [Object[]] objects array of objects to test if a valid array
|
29
|
+
#
|
30
|
+
# @raise [ArgumentError] if the argument is not an [Array]
|
31
|
+
# @return [nil] nil
|
32
|
+
#
|
33
|
+
def raise_if_not_an_array(objects)
|
34
|
+
raise_if_not_a(objects, Array)
|
35
|
+
nil
|
36
|
+
end
|
37
|
+
|
38
|
+
#
|
39
|
+
# Raise an error if any object in the array is not of a type Hash
|
40
|
+
#
|
41
|
+
# @param [Object[]] objects array of objects to test if a valid hash
|
42
|
+
#
|
43
|
+
# @raise [ArgumentError] if the argument is not a [Hash]
|
44
|
+
# @return [nil] nil
|
45
|
+
#
|
46
|
+
def raise_if_not_a_hash(objects)
|
47
|
+
raise_if_not_a(objects, Hash)
|
48
|
+
nil
|
49
|
+
end
|
50
|
+
|
51
|
+
#
|
52
|
+
# Raise an error if any keywords are not included inside of a specified binding.
|
53
|
+
#
|
54
|
+
# @param [Object] keywords list of keywords to check.
|
55
|
+
# @param [Binding] a_binding binding to check for local variables
|
56
|
+
#
|
57
|
+
# @raise [ArgumentError] if the argument is not of the specified type.
|
58
|
+
# @return [nil] nil
|
59
|
+
#
|
60
|
+
def required_arguments(keywords, a_binding)
|
61
|
+
keywords.each { |arg| Exceptions.raise_unless_required_keyword?(keyword: a_binding.local_variable_get(arg)) }
|
62
|
+
nil
|
63
|
+
end
|
64
|
+
end
|
65
|
+
end
|
66
|
+
end
|
67
|
+
end
|