active-record-binder 1.1.0 → 1.2.0
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/README.md +217 -0
- data/bin/arb +13 -0
- data/doc/Binder.html +150 -0
- data/doc/Binder/AR.html +1880 -0
- data/doc/Binder/Command.html +252 -0
- data/doc/Binder/Help.html +374 -0
- data/doc/Binder/Migrate.html +682 -0
- data/doc/Binder/Strategy.html +550 -0
- data/doc/Binder/Version.html +285 -0
- data/doc/Class.html +220 -0
- data/doc/CommandParser.html +268 -0
- data/doc/CommandParser/ParseError.html +123 -0
- data/doc/DeferedDelegator.html +414 -0
- data/doc/MigrationProcessError.html +123 -0
- data/doc/MigrationVersionError.html +123 -0
- data/doc/String.html +245 -0
- data/doc/_index.html +256 -0
- data/doc/class_list.html +53 -0
- data/doc/css/common.css +1 -0
- data/doc/css/full_list.css +57 -0
- data/doc/css/style.css +328 -0
- data/doc/file.README.html +300 -0
- data/doc/file_list.html +55 -0
- data/doc/frames.html +28 -0
- data/doc/index.html +300 -0
- data/doc/js/app.js +214 -0
- data/doc/js/full_list.js +173 -0
- data/doc/js/jquery.js +4 -0
- data/doc/method_list.html +348 -0
- data/doc/top-level-namespace.html +114 -0
- data/extras/cli_help.png +0 -0
- data/lib/active_record_binder.rb +21 -37
- data/lib/cli/command.rb +101 -0
- data/lib/cli/command_parser.rb +29 -0
- data/lib/cli/commands/commands.rb +4 -0
- data/lib/cli/commands/help.rb +35 -0
- data/lib/cli/commands/migrate.rb +77 -0
- data/lib/cli/commands/version.rb +16 -0
- data/lib/cli/core_ext.rb +21 -0
- data/lib/defered_delegator.rb +69 -0
- data/lib/version.rb +3 -0
- data/test/active_record_binder_test.rb +262 -0
- data/test/foo.sqlite3 +0 -0
- data/test/migrations.rb +29 -0
- data/test/minitest_helper.rb +15 -0
- data/test/mocks.rb +24 -0
- metadata +62 -5
@@ -0,0 +1,114 @@
|
|
1
|
+
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN"
|
2
|
+
"http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
|
3
|
+
<html xmlns="http://www.w3.org/1999/xhtml" xml:lang="en" lang="en">
|
4
|
+
<head>
|
5
|
+
<meta http-equiv="Content-Type" content="text/html; charset=utf-8" />
|
6
|
+
<title>
|
7
|
+
Top Level Namespace
|
8
|
+
|
9
|
+
— Documentation by YARD 0.8.4.1
|
10
|
+
|
11
|
+
</title>
|
12
|
+
|
13
|
+
<link rel="stylesheet" href="css/style.css" type="text/css" media="screen" charset="utf-8" />
|
14
|
+
|
15
|
+
<link rel="stylesheet" href="css/common.css" type="text/css" media="screen" charset="utf-8" />
|
16
|
+
|
17
|
+
<script type="text/javascript" charset="utf-8">
|
18
|
+
hasFrames = window.top.frames.main ? true : false;
|
19
|
+
relpath = '';
|
20
|
+
framesUrl = "frames.html#!" + escape(window.location.href);
|
21
|
+
</script>
|
22
|
+
|
23
|
+
|
24
|
+
<script type="text/javascript" charset="utf-8" src="js/jquery.js"></script>
|
25
|
+
|
26
|
+
<script type="text/javascript" charset="utf-8" src="js/app.js"></script>
|
27
|
+
|
28
|
+
|
29
|
+
</head>
|
30
|
+
<body>
|
31
|
+
<div id="header">
|
32
|
+
<div id="menu">
|
33
|
+
|
34
|
+
<a href="_index.html">Index</a> »
|
35
|
+
|
36
|
+
|
37
|
+
<span class="title">Top Level Namespace</span>
|
38
|
+
|
39
|
+
|
40
|
+
<div class="noframes"><span class="title">(</span><a href="." target="_top">no frames</a><span class="title">)</span></div>
|
41
|
+
</div>
|
42
|
+
|
43
|
+
<div id="search">
|
44
|
+
|
45
|
+
<a class="full_list_link" id="class_list_link"
|
46
|
+
href="class_list.html">
|
47
|
+
Class List
|
48
|
+
</a>
|
49
|
+
|
50
|
+
<a class="full_list_link" id="method_list_link"
|
51
|
+
href="method_list.html">
|
52
|
+
Method List
|
53
|
+
</a>
|
54
|
+
|
55
|
+
<a class="full_list_link" id="file_list_link"
|
56
|
+
href="file_list.html">
|
57
|
+
File List
|
58
|
+
</a>
|
59
|
+
|
60
|
+
</div>
|
61
|
+
<div class="clear"></div>
|
62
|
+
</div>
|
63
|
+
|
64
|
+
<iframe id="search_frame"></iframe>
|
65
|
+
|
66
|
+
<div id="content"><h1>Top Level Namespace
|
67
|
+
|
68
|
+
|
69
|
+
|
70
|
+
</h1>
|
71
|
+
|
72
|
+
<dl class="box">
|
73
|
+
|
74
|
+
|
75
|
+
|
76
|
+
|
77
|
+
|
78
|
+
|
79
|
+
|
80
|
+
|
81
|
+
</dl>
|
82
|
+
<div class="clear"></div>
|
83
|
+
|
84
|
+
<h2>Defined Under Namespace</h2>
|
85
|
+
<p class="children">
|
86
|
+
|
87
|
+
|
88
|
+
<strong class="modules">Modules:</strong> <span class='object_link'><a href="Binder.html" title="Binder (module)">Binder</a></span>, <span class='object_link'><a href="CommandParser.html" title="CommandParser (module)">CommandParser</a></span>, <span class='object_link'><a href="DeferedDelegator.html" title="DeferedDelegator (module)">DeferedDelegator</a></span>
|
89
|
+
|
90
|
+
|
91
|
+
|
92
|
+
<strong class="classes">Classes:</strong> <span class='object_link'><a href="Class.html" title="Class (class)">Class</a></span>, <span class='object_link'><a href="MigrationProcessError.html" title="MigrationProcessError (class)">MigrationProcessError</a></span>, <span class='object_link'><a href="MigrationVersionError.html" title="MigrationVersionError (class)">MigrationVersionError</a></span>, <span class='object_link'><a href="String.html" title="String (class)">String</a></span>
|
93
|
+
|
94
|
+
|
95
|
+
</p>
|
96
|
+
|
97
|
+
|
98
|
+
|
99
|
+
|
100
|
+
|
101
|
+
|
102
|
+
|
103
|
+
|
104
|
+
|
105
|
+
</div>
|
106
|
+
|
107
|
+
<div id="footer">
|
108
|
+
Generated on Thu Feb 21 02:06:16 2013 by
|
109
|
+
<a href="http://yardoc.org" title="Yay! A Ruby Documentation Tool" target="_parent">yard</a>
|
110
|
+
0.8.4.1 (ruby-1.9.3).
|
111
|
+
</div>
|
112
|
+
|
113
|
+
</body>
|
114
|
+
</html>
|
data/extras/cli_help.png
ADDED
Binary file
|
data/lib/active_record_binder.rb
CHANGED
@@ -1,33 +1,9 @@
|
|
1
1
|
require 'active_record'
|
2
2
|
require 'active_support/core_ext/string'
|
3
|
-
|
4
|
-
|
5
|
-
# Private: A simple module that delegates classes methods when needed, keeping the calls in memory.
|
6
|
-
module DifferedDelegator
|
7
|
-
def register_delegators *args
|
8
|
-
args.each do |delegator|
|
9
|
-
delegator = delegator.to_s
|
10
|
-
module_eval %Q{
|
11
|
-
def self.#{delegator} *parameters
|
12
|
-
@delegators ||= []
|
13
|
-
@delegators << { name: :#{delegator}, params: parameters }
|
14
|
-
end
|
15
|
-
}
|
16
|
-
end
|
17
|
-
end
|
18
|
-
|
19
|
-
def delegate_to klass_or_object
|
20
|
-
@delegators.each do |data|
|
21
|
-
unless data.empty?
|
22
|
-
name = data[:name]
|
23
|
-
args = data[:params]
|
24
|
-
klass_or_object.send(name, *args)
|
25
|
-
end
|
26
|
-
end
|
27
|
-
end
|
28
|
-
end
|
3
|
+
require_relative './defered_delegator'
|
29
4
|
|
30
5
|
class MigrationVersionError < Exception; end
|
6
|
+
class MigrationProcessError < Exception; end
|
31
7
|
|
32
8
|
# Public: Namespace containing classes to create binder to.
|
33
9
|
# A binder is simply a tool to use for Databases plugs or adaptors building.
|
@@ -73,7 +49,7 @@ module Binder
|
|
73
49
|
# ARMySqlPlug::connection # => { :user => 'Foo', :password => 'Bar', :host => 'localhost' }
|
74
50
|
#
|
75
51
|
class AR
|
76
|
-
extend
|
52
|
+
extend DeferedDelegator
|
77
53
|
|
78
54
|
attr_reader :table_name, :table
|
79
55
|
register_delegators :has_many, :has_one, :has_and_belongs_to_many, :belongs_to
|
@@ -93,10 +69,10 @@ module Binder
|
|
93
69
|
# Returns an instance of the binder's class.
|
94
70
|
def initialize table_name
|
95
71
|
@table_name = table_name
|
96
|
-
this = self.class
|
97
72
|
|
98
73
|
# Retrieves or Create the ActiveRecord::Base subclass that will match the table.
|
99
|
-
table = meta_def_ar_class
|
74
|
+
table = meta_def_ar_class
|
75
|
+
|
100
76
|
# Handle ActiveRecord::Base delegation, to ensure painless associations
|
101
77
|
self.class.delegate_to table
|
102
78
|
|
@@ -112,7 +88,7 @@ module Binder
|
|
112
88
|
# _params - the connection parameters
|
113
89
|
#
|
114
90
|
# Returns the class object.
|
115
|
-
def meta_def_ar_class
|
91
|
+
def meta_def_ar_class
|
116
92
|
klass = table_name.to_s.classify
|
117
93
|
binder = self.class
|
118
94
|
|
@@ -123,13 +99,7 @@ module Binder
|
|
123
99
|
binder.const_set(klass,
|
124
100
|
Class.new(ActiveRecord::Base) do # class `TableName` < ActiveRecord::Base
|
125
101
|
singleton_class.send(:define_method, :connect) do # def self.connect
|
126
|
-
|
127
|
-
# We ensure we have a string for the adapter
|
128
|
-
opts[:adapter] = opts[:adapter].to_s
|
129
|
-
# If we have a symbol for the database and the adapter is sqlite3, we create a string and add '.sqlite3' to the end
|
130
|
-
opts[:database] = "#{opts[:database]}.sqlite3" if opts[:adapter] == 'sqlite3' and opts[:database].class == Symbol
|
131
|
-
|
132
|
-
ActiveRecord::Base.establish_connection(opts)
|
102
|
+
ActiveRecord::Base.establish_connection(binder.connection_data)
|
133
103
|
end # end
|
134
104
|
end) # end
|
135
105
|
end #if
|
@@ -194,6 +164,18 @@ module Binder
|
|
194
164
|
end
|
195
165
|
alias :connect_with :connection
|
196
166
|
|
167
|
+
# Public: Retrieves a clean set of connection data to establish a connection
|
168
|
+
#
|
169
|
+
# Returns a Hash.
|
170
|
+
def connection_data
|
171
|
+
opts = { database: self.database, adapter: self.adapter }.merge(self.connection)
|
172
|
+
# We ensure we have a string for the adapter
|
173
|
+
opts[:adapter] = opts[:adapter].to_s
|
174
|
+
# If we have a symbol for the database and the adapter is sqlite3, we create a string and add '.sqlite3' to the end
|
175
|
+
opts[:database] = "#{opts[:database]}.sqlite3" if opts[:adapter] == 'sqlite3' and opts[:database].class == Symbol
|
176
|
+
opts
|
177
|
+
end
|
178
|
+
|
197
179
|
# Public: Retrieves de default database
|
198
180
|
#
|
199
181
|
# Returns a the content of ENV\['APP_DB'\].
|
@@ -324,6 +306,8 @@ module Binder
|
|
324
306
|
#
|
325
307
|
# Returns Nothing.
|
326
308
|
def __create_meta_data_table_for schema
|
309
|
+
ActiveRecord::Base.establish_connection(self.connection_data) unless schema.connected?
|
310
|
+
|
327
311
|
# Clears the table cache for the schema (remove TableDoesNotExists if a table actually exists)
|
328
312
|
schema.clear_cache!
|
329
313
|
|
data/lib/cli/command.rb
ADDED
@@ -0,0 +1,101 @@
|
|
1
|
+
require_relative './command_parser'
|
2
|
+
require_relative './core_ext'
|
3
|
+
|
4
|
+
module Binder
|
5
|
+
# Public: A Class to create Command Line Tool commands.
|
6
|
+
#
|
7
|
+
# Examples
|
8
|
+
#
|
9
|
+
# Binder::Command.new ARGV
|
10
|
+
# # => Will execute any command passed in ARGV
|
11
|
+
# # A command is just a ruby Class, subclassing the Binder::Strategy Class.
|
12
|
+
# #
|
13
|
+
# # When you do :
|
14
|
+
# Binder::Command.new "migrate --version 1.1"
|
15
|
+
# # => The Binder::Migrate class is instanciated and it's execute method is called.
|
16
|
+
class Command
|
17
|
+
def initialize args
|
18
|
+
raise CommandParser::ParseError.new("No command specified.") if args.empty?
|
19
|
+
|
20
|
+
# ['--migrate' 'param'] => 'migrate'
|
21
|
+
command = args.shift.gsub(/\-/, '')
|
22
|
+
|
23
|
+
strategy = Binder::const_get(command.capitalize).new
|
24
|
+
puts strategy.execute args
|
25
|
+
end
|
26
|
+
end
|
27
|
+
|
28
|
+
# Public: A Strategy is a way to declare a specific command.
|
29
|
+
# You need to subclass the Binder::Strategy class and declare an `execute` method and a `description` method.
|
30
|
+
#
|
31
|
+
# Examples
|
32
|
+
#
|
33
|
+
# class Migrate < Binder::Strategy
|
34
|
+
# def execute args
|
35
|
+
# # Parse args and do migration stuff
|
36
|
+
# "Migration Done." # <= Binder::Command.new automaticaly renders the return value of an `execute` call
|
37
|
+
# end
|
38
|
+
# #
|
39
|
+
# # The description call is mainly used by the `help` command.
|
40
|
+
# def description
|
41
|
+
# "Easier Migration. Use the " + "--directory".colorize(:orange) + " option to pass in a directory to load."
|
42
|
+
# # Yeah, notice the "colorize" String method that helps you write a string in a beatiful color.
|
43
|
+
# end
|
44
|
+
# end
|
45
|
+
class Strategy
|
46
|
+
# Public: creates an alias for the command.
|
47
|
+
#
|
48
|
+
# Examples
|
49
|
+
#
|
50
|
+
# class Migrate < Binder::Strategy
|
51
|
+
# # def execute...
|
52
|
+
# # def description...
|
53
|
+
# #
|
54
|
+
# alias_class :M
|
55
|
+
# # => This will create an alias :M class and this the existance of the corresponding "-m" command. (`arb --migrate` or `arb -m`, now)
|
56
|
+
# end
|
57
|
+
def self.alias_class _alias
|
58
|
+
Binder::const_set(_alias, Class.new(self))
|
59
|
+
end
|
60
|
+
|
61
|
+
# Public: returns the String size of the biggest command or option group.
|
62
|
+
#
|
63
|
+
# Returns a Numeric.
|
64
|
+
def justify_size
|
65
|
+
if @options.nil?
|
66
|
+
Binder::Strategy.subclasses.map(&:to_s).group_by(&:size).max.flatten.first - "Binder".length
|
67
|
+
else
|
68
|
+
@options.group_by(&:size).max.flatten.first
|
69
|
+
end
|
70
|
+
end
|
71
|
+
|
72
|
+
# Public: merge options and aliases "-m, --migrate, -h, --help", etc...
|
73
|
+
#
|
74
|
+
# Uses an array of commands : ["-h" "-m", "--help", "--migrate"],
|
75
|
+
#
|
76
|
+
# and concatenate the aliases : ["-m, --migrate", "-h, --help"]
|
77
|
+
#
|
78
|
+
# Returns an Array of merged aliases.
|
79
|
+
def merge_options_aliases
|
80
|
+
commands = Binder::Strategy.subclasses.map { |command| command.to_s.gsub!('Binder::', '').downcase }.sort
|
81
|
+
previous_cmd = ""
|
82
|
+
options = []
|
83
|
+
|
84
|
+
commands.each_with_index do |cmd, i|
|
85
|
+
if cmd.length == 1
|
86
|
+
previous_cmd = cmd
|
87
|
+
elsif not previous_cmd.empty?
|
88
|
+
cmd_prefixes = [previous_cmd.length == 1 ? '-' : '--', cmd.length == 1 ? '-' : '--']
|
89
|
+
options << "#{cmd_prefixes.first}#{previous_cmd}, #{cmd_prefixes.last}#{cmd}"
|
90
|
+
previous_cmd = ""
|
91
|
+
end
|
92
|
+
end
|
93
|
+
@options = options
|
94
|
+
end
|
95
|
+
|
96
|
+
# Public: provides a default description if not defined.
|
97
|
+
#
|
98
|
+
# Returns a "No description found" String.
|
99
|
+
def description; "No description found" end
|
100
|
+
end
|
101
|
+
end
|
@@ -0,0 +1,29 @@
|
|
1
|
+
module CommandParser
|
2
|
+
# Public: Parse options and creates an array of hash data for each.
|
3
|
+
#
|
4
|
+
# args - An Array of options and parameters to parse.
|
5
|
+
#
|
6
|
+
# Examples
|
7
|
+
#
|
8
|
+
# CommandParser::parse_options "--directory lib/ migrations/ --adapter MySqlPlug"
|
9
|
+
# # => [{ option: 'directory', option_args: ["lib/", "migrations/"] }, { option: 'adapter', option_args: ["MySqlPlug"] }]
|
10
|
+
#
|
11
|
+
# Returns a parsed array.
|
12
|
+
def self.parse_options(args)
|
13
|
+
parsed = []
|
14
|
+
last_option = ""
|
15
|
+
args.each do |chunk|
|
16
|
+
if chunk[/^-*/].empty?
|
17
|
+
raise CommandParser::ParseError.new("Arguments given but no option has been specified.") if last_option.empty?
|
18
|
+
cur_cmd = parsed.last
|
19
|
+
cur_cmd[:options_args] ||= []
|
20
|
+
cur_cmd[:options_args] << chunk
|
21
|
+
else
|
22
|
+
parsed << { option: chunk.sub(/^-*/, '') } and last_option = chunk
|
23
|
+
end
|
24
|
+
end
|
25
|
+
parsed
|
26
|
+
end
|
27
|
+
|
28
|
+
class CommandParser::ParseError < StandardError; end
|
29
|
+
end
|
@@ -0,0 +1,35 @@
|
|
1
|
+
require_relative './version'
|
2
|
+
|
3
|
+
module Binder
|
4
|
+
# Public: [Command] Displays the help for `arb`. Used via the `arb` command.
|
5
|
+
class Help < Binder::Strategy
|
6
|
+
def execute args
|
7
|
+
instructions = []
|
8
|
+
help_topics = merge_options_aliases
|
9
|
+
|
10
|
+
help_topics.each do |topic|
|
11
|
+
binder = Module.nesting.last
|
12
|
+
|
13
|
+
cmd_name = topic.split(" ").last.gsub(/^-*/, '').capitalize
|
14
|
+
command = binder.const_get(cmd_name)
|
15
|
+
cmd_description = command.new.description
|
16
|
+
|
17
|
+
instructions << format_instruction(topic.to_s, cmd_description)
|
18
|
+
end
|
19
|
+
|
20
|
+
Version.new.execute(nil) + "\n" +
|
21
|
+
instructions.join("\n")
|
22
|
+
end
|
23
|
+
|
24
|
+
|
25
|
+
def format_instruction name, description
|
26
|
+
" #{name.ljust(justify_size).colorize(:green)} - #{description}"
|
27
|
+
end
|
28
|
+
|
29
|
+
def description
|
30
|
+
"Displays the help"
|
31
|
+
end
|
32
|
+
|
33
|
+
alias_class :H
|
34
|
+
end
|
35
|
+
end
|
@@ -0,0 +1,77 @@
|
|
1
|
+
require 'active_record_binder'
|
2
|
+
|
3
|
+
module Binder
|
4
|
+
# Public: [Command] Runs your migrations. Used via the `arb` command.
|
5
|
+
class Migrate < Binder::Strategy
|
6
|
+
def execute args
|
7
|
+
commands = CommandParser::parse_options(args)
|
8
|
+
commands.each do |c|
|
9
|
+
option = c[:option]
|
10
|
+
args = c[:options_args]
|
11
|
+
self.send(option.to_sym, args)
|
12
|
+
end
|
13
|
+
"\nDone."
|
14
|
+
end
|
15
|
+
|
16
|
+
def version version
|
17
|
+
puts "Migrating toward version #{version.first.to_f.to_s.colorize(:orange)}"
|
18
|
+
@version ||= version.first.to_f
|
19
|
+
end
|
20
|
+
alias :v :version
|
21
|
+
alias :to :version
|
22
|
+
|
23
|
+
def plug args
|
24
|
+
args.each do |klass|
|
25
|
+
raise MigrationProcessError.new("Plug #{klass.colorize(:red)} not found.") unless Object.const_defined?(klass)
|
26
|
+
Object.const_get(klass).migrate @version
|
27
|
+
end
|
28
|
+
end
|
29
|
+
alias :a :plug
|
30
|
+
alias :adapter :plug
|
31
|
+
alias :adaptor :plug
|
32
|
+
|
33
|
+
def directory blobs, recursively = false
|
34
|
+
blobs.each do |blob|
|
35
|
+
blob = File::absolute_path(blob)
|
36
|
+
raise CommandParser::ParseError.new("Invalid argument : no such file or directory #{blob}.") unless File::exists?(blob)
|
37
|
+
|
38
|
+
if File::directory?(blob)
|
39
|
+
files = Dir.glob("#{blob}/*")
|
40
|
+
files.each do |file|
|
41
|
+
if recursively == true && File::directory?(file)
|
42
|
+
directory [file], true
|
43
|
+
end
|
44
|
+
# Requires the file and logs it on stdout
|
45
|
+
puts _require_path(file) unless File::directory?(file)
|
46
|
+
end
|
47
|
+
else
|
48
|
+
puts _require_path(blob)
|
49
|
+
end
|
50
|
+
end
|
51
|
+
end
|
52
|
+
def _require_path path; "Requiring #{path.colorize(:green)}: #{require(path).to_s.colorize(:orange)}" end
|
53
|
+
alias :d :directory
|
54
|
+
alias :file :directory
|
55
|
+
alias :f :directory
|
56
|
+
|
57
|
+
def recursive blobs
|
58
|
+
directory blobs, true
|
59
|
+
end
|
60
|
+
alias :r :recursive
|
61
|
+
|
62
|
+
def description
|
63
|
+
indent = ' ' * (justify_size * 2)
|
64
|
+
|
65
|
+
str = []
|
66
|
+
str << "Migrate to the specified version"
|
67
|
+
str << "#{indent}" + "-v".colorize(:rose) + ", --version".colorize(:rose) + ", --to".colorize(:rose) + " - Version we want to migrate to"
|
68
|
+
str << "#{indent}" + "-d".colorize(:rose) + ", --directory".colorize(:rose) + " - Allow to specify a set of directories holding your migrations"
|
69
|
+
str << "#{indent}" + "-r".colorize(:rose) + ", --recursive".colorize(:rose) + " - Same as `--directory` but will search recursively in the directories"
|
70
|
+
str << "#{indent}" + "-f".colorize(:rose) + ", --file".colorize(:rose) + " - Allow to specify a set of files holding your migrations"
|
71
|
+
str << "#{indent}" + "-a".colorize(:rose) + ", --adapter".colorize(:rose) + ", --plug".colorize(:rose) + " - Specify a Binder class on which to run the migration"
|
72
|
+
str.join("\n")
|
73
|
+
end
|
74
|
+
|
75
|
+
alias_class :M
|
76
|
+
end
|
77
|
+
end
|