ing 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.
Files changed (59) hide show
  1. data/.gitignore +5 -0
  2. data/GENERATORS.md +2 -0
  3. data/LICENSE +18 -0
  4. data/OPTIONS.md +2 -0
  5. data/README.md +251 -0
  6. data/TASKS.md +21 -0
  7. data/bin/ing +5 -0
  8. data/examples/rspec_convert.rb +102 -0
  9. data/ing.gemspec +29 -0
  10. data/ing.rb +102 -0
  11. data/lib/ing.rb +78 -0
  12. data/lib/ing/actions/create_file.rb +105 -0
  13. data/lib/ing/actions/create_link.rb +57 -0
  14. data/lib/ing/actions/directory.rb +98 -0
  15. data/lib/ing/actions/empty_directory.rb +155 -0
  16. data/lib/ing/actions/file_manipulation.rb +308 -0
  17. data/lib/ing/actions/inject_into_file.rb +109 -0
  18. data/lib/ing/commands/boot.rb +76 -0
  19. data/lib/ing/commands/generate.rb +64 -0
  20. data/lib/ing/commands/help.rb +87 -0
  21. data/lib/ing/commands/implicit.rb +59 -0
  22. data/lib/ing/commands/list.rb +108 -0
  23. data/lib/ing/dispatcher.rb +132 -0
  24. data/lib/ing/files.rb +190 -0
  25. data/lib/ing/lib_trollop.rb +782 -0
  26. data/lib/ing/shell.rb +390 -0
  27. data/lib/ing/trollop/parser.rb +17 -0
  28. data/lib/ing/util.rb +61 -0
  29. data/lib/ing/version.rb +3 -0
  30. data/lib/thor/actions/file_manipulation.rb +30 -0
  31. data/lib/thor/shell/basic.rb +44 -0
  32. data/test/acceptance/ing_run_tests.rb +164 -0
  33. data/test/actions/create_file_spec.rb +209 -0
  34. data/test/actions/create_link_spec.rb +90 -0
  35. data/test/actions/directory_spec.rb +167 -0
  36. data/test/actions/empty_directory_spec.rb +146 -0
  37. data/test/actions/file_manipulation_spec.rb +433 -0
  38. data/test/actions/inject_into_file_spec.rb +147 -0
  39. data/test/fixtures/application.rb +2 -0
  40. data/test/fixtures/app{1}/README +3 -0
  41. data/test/fixtures/bundle/execute.rb +6 -0
  42. data/test/fixtures/bundle/main.thor +1 -0
  43. data/test/fixtures/doc/%file_name%.rb.tt +1 -0
  44. data/test/fixtures/doc/COMMENTER +10 -0
  45. data/test/fixtures/doc/README +3 -0
  46. data/test/fixtures/doc/block_helper.rb +3 -0
  47. data/test/fixtures/doc/components/.empty_directory +0 -0
  48. data/test/fixtures/doc/config.rb +1 -0
  49. data/test/fixtures/doc/config.yaml.tt +1 -0
  50. data/test/fixtures/group.ing.rb +76 -0
  51. data/test/fixtures/invok.ing.rb +50 -0
  52. data/test/fixtures/namespace.ing.rb +52 -0
  53. data/test/fixtures/require.ing.rb +7 -0
  54. data/test/fixtures/task.ing.rb +36 -0
  55. data/test/fixtures/task.thor +10 -0
  56. data/test/spec_helper.rb +2 -0
  57. data/test/test_helper.rb +41 -0
  58. data/todo.yml +7 -0
  59. metadata +147 -0
data/ing.rb ADDED
@@ -0,0 +1,102 @@
1
+ # Usage:
2
+ # ing rspec:convert which is equivalent to
3
+ # ing rspec:convert files './{test,spec}/**/*.rb' --convert-dir 'converted'
4
+ #
5
+ module Rspec
6
+
7
+ class Convert
8
+
9
+ GSUBS = \
10
+ [
11
+ [ /^(\s*)(.+)\.should\s+be_true/ , '\1assert \2' ],
12
+ [ /^(\s*)(.+)\.should\s+be_false/ , '\1refute \2' ],
13
+ [ /^(\s*)(.+)\.should\s*==\s*(.+)$/ , '\1assert_equal \3, \2' ],
14
+ [ /^(\s*)(.+)\.should_not\s*==\s*(.+)$/ , '\1refute_equal \3, \2' ],
15
+ [ /^(\s*)(.+)\.should\s*=~\s*(.+)$/ , '\1assert_match(\3, \2)' ],
16
+ [ /^(\s*)(.+)\.should_not\s*=~\s*(.+)$/ , '\1refute_match(\3, \2)' ],
17
+ [ /^(\s*)(.+)\.should\s+be_(.+)$/ , '\1assert \2.\3?' ],
18
+ [ /^(\s*)(.+)\.should_not\s+be_(.+)$/ , '\1refute \2.\3?' ],
19
+ [ /expect\s+\{(.+)\}\.to\s+raise_error\s*\((.*)\)\s*\Z/m,
20
+ 'assert_raises(\2) {\1}' ],
21
+ [ /\{(.+)\}\.should raise_error\s*\((.*)\)\s*\Z/m,
22
+ 'assert_raises(\2) {\1}' ],
23
+ # these next aren't quite right because they need to wrap the next
24
+ # lines as a lambda. Thus the FIXME notes.
25
+ [ /\.should_receive\(([^\)]+)\)\.and_return\((.+)\)/,
26
+ '.stub(\1, \2) do |s| # FIXME' ],
27
+ [ /.stub\!\(([\w:]+)\)\.and_return\((.+)\)/,
28
+ '.stub(\1, \2) do |s| # FIXME' ]
29
+
30
+ ]
31
+
32
+ def self.specify_options(expect)
33
+ expect.banner "Convert rspec 'should/not' matchers to minitest 'assert/refute'"
34
+ expect.text "It's not magic, you still need to hand edit your test files after running this"
35
+ expect.text "\nUsage:"
36
+ expect.text " ing rspec:convert # which is equivalent to"
37
+ expect.text " ing rspec:convert files './{test,spec}/**/*.rb' --convert-dir 'converted'"
38
+ expect.text "\nOptions:"
39
+ expect.opt :pattern, "Directory glob pattern for test files",
40
+ :type => :string, :default => './{test,spec}/**/*.rb'
41
+ expect.opt :convert_dir, "Subdirectory to save converted files",
42
+ :type => :string, :default => 'converted'
43
+ end
44
+
45
+ include Ing::Files
46
+
47
+ attr_accessor :shell, :options, :destination_root, :source_root
48
+
49
+ def destination_root; @destination_root ||= Dir.pwd; end
50
+ def source_root; @source_root ||= File.dirname(__FILE__); end
51
+
52
+ def input_files
53
+ @input_files ||= Dir[ File.expand_path(options[:pattern], source_root) ]
54
+ end
55
+
56
+ def converted_files
57
+ input_files.map {|f|
58
+ File.join( File.dirname(f), options[:convert_dir], File.basename(f) )
59
+ }
60
+ end
61
+
62
+ def conversion_map
63
+ input_files.zip(converted_files)
64
+ end
65
+
66
+ def initialize(options)
67
+ self.options = options
68
+ end
69
+
70
+ def call(pattern=nil)
71
+ options[:pattern] = pattern || options[:pattern]
72
+ shell.say "Processing #{input_files.length} input files: #{options[:pattern]}" if verbose?
73
+ conversion_map.each do |input_f, output_f|
74
+ new_lines = convert_lines(input_f)
75
+ create_file output_f, new_lines.join
76
+ end
77
+ end
78
+ alias :files :call
79
+
80
+ private
81
+
82
+ def convert_lines(fname)
83
+ count = 0; accum = []
84
+ File.open(fname) do |f|
85
+ f.each_line do |line|
86
+ new_line = GSUBS.inject(line) do |str, (rx, replace)|
87
+ str = str.gsub(rx, replace)
88
+ end
89
+ count += 1 if line != new_line
90
+ accum << new_line
91
+ end
92
+ end
93
+ shell.say_status(:convert,
94
+ "#{relative_to_original_destination_root(fname)}: " +
95
+ "#{count} changes",
96
+ :green) if verbose?
97
+ accum
98
+ end
99
+
100
+ end
101
+
102
+ end
data/lib/ing.rb ADDED
@@ -0,0 +1,78 @@
1
+ ['ing/version',
2
+ 'ing/lib_trollop',
3
+ 'ing/trollop/parser',
4
+ 'ing/util',
5
+ 'ing/dispatcher',
6
+ 'ing/shell',
7
+ 'ing/files',
8
+ 'ing/commands/boot',
9
+ 'ing/commands/implicit',
10
+ 'ing/commands/list',
11
+ 'ing/commands/help',
12
+ 'ing/commands/generate'
13
+ ].each do |f|
14
+ require_relative f
15
+ end
16
+
17
+ module Ing
18
+ extend self
19
+
20
+ Error = Class.new(StandardError)
21
+ FileNotFoundError = Class.new(Error)
22
+
23
+ attr_writer :shell_class
24
+
25
+ def shell_class
26
+ @shell_class ||= Shell::Basic
27
+ end
28
+
29
+ def implicit_booter
30
+ ["Implicit"]
31
+ end
32
+
33
+ # Dispatch command line to boot class (if specified, or Implicit otherwise),
34
+ # which in turn dispatches the command after parsing args.
35
+ #
36
+ # Note boot dispatch happens within +Ing::Commands+ namespace.
37
+ #
38
+ def run(argv=ARGV)
39
+ booter = extract_boot_class!(argv) || implicit_booter
40
+ run_boot booter, "call", *argv
41
+ end
42
+
43
+ # Dispatch to the command via +Ing::Boot#call_invoke+
44
+ # Use this when you want to invoke a command from another command, but only
45
+ # if it hasn't been run yet. For example,
46
+ #
47
+ # invoke Some::Task, :some_instance_method, some_argument, :some_option => true
48
+ #
49
+ # You can skip the method and it will assume +#call+ :
50
+ #
51
+ # invoke Some::Task, :some_option => true
52
+ def invoke(klass, *args)
53
+ run_boot implicit_booter, "call_invoke", klass, *args
54
+ end
55
+
56
+ # Dispatch to the command via +Ing::Boot#call_execute+
57
+ # Use this when you want to execute a command from another command, and you
58
+ # don't care if it has been run yet or not. See equivalent examples for
59
+ # +invoke+.
60
+ #
61
+ def execute(klass, *args)
62
+ run_boot implicit_booter, "call_execute", klass, *args
63
+ end
64
+
65
+ private
66
+
67
+ def run_boot(booter, *args)
68
+ Dispatcher.new(["Ing","Commands"], booter, *args).dispatch
69
+ end
70
+
71
+ def extract_boot_class!(args)
72
+ c = Util.to_class_names(args.first)
73
+ if (Commands.const_defined?(c.first, false) rescue nil)
74
+ args.shift; c
75
+ end
76
+ end
77
+
78
+ end
@@ -0,0 +1,105 @@
1
+ require File.expand_path('empty_directory', File.dirname(__FILE__))
2
+
3
+ module Ing
4
+ module Files
5
+
6
+ # Create a new file relative to the destination root with the given data,
7
+ # which is the return value of a block or a data string.
8
+ #
9
+ # ==== Parameters
10
+ # destination<String>:: the relative path to the destination root.
11
+ # data<String|NilClass>:: the data to append to the file.
12
+ # config<Hash>:: give :verbose => false to not log the status.
13
+ #
14
+ # ==== Examples
15
+ #
16
+ # create_file "lib/fun_party.rb" do
17
+ # hostname = ask("What is the virtual hostname I should use?")
18
+ # "vhost.name = #{hostname}"
19
+ # end
20
+ #
21
+ # create_file "config/apache.conf", "your apache config"
22
+ #
23
+ def create_file(destination, *args, &block)
24
+ config = args.last.is_a?(Hash) ? args.pop : {}
25
+ data = args.first
26
+ action CreateFile.new(self, destination, block || data.to_s, config)
27
+ end
28
+ alias :add_file :create_file
29
+
30
+ # CreateFile is a subset of Template, which instead of rendering a file with
31
+ # ERB, it gets the content from the user.
32
+ #
33
+ class CreateFile < EmptyDirectory #:nodoc:
34
+ attr_reader :data
35
+
36
+ def initialize(base, destination, data, config={})
37
+ @data = data
38
+ super(base, destination, config)
39
+ end
40
+
41
+ # Checks if the content of the file at the destination is identical to the rendered result.
42
+ #
43
+ # ==== Returns
44
+ # Boolean:: true if it is identical, false otherwise.
45
+ #
46
+ def identical?
47
+ exists? && File.binread(destination) == render
48
+ end
49
+
50
+ # Holds the content to be added to the file.
51
+ #
52
+ def render
53
+ @render ||= if data.is_a?(Proc)
54
+ data.call
55
+ else
56
+ data
57
+ end
58
+ end
59
+
60
+ def invoke!
61
+ invoke_with_conflict_check do
62
+ FileUtils.mkdir_p(File.dirname(destination))
63
+ File.open(destination, 'wb') { |f| f.write render }
64
+ end
65
+ given_destination
66
+ end
67
+
68
+ protected
69
+
70
+ # Now on conflict we check if the file is identical or not.
71
+ #
72
+ def on_conflict_behavior(&block)
73
+ if identical?
74
+ say_status :identical, :blue
75
+ else
76
+ options = base.options.merge(config)
77
+ force_or_skip_or_conflict(options[:force], options[:skip], &block)
78
+ end
79
+ end
80
+
81
+ # If force is true, run the action, otherwise check if it's not being
82
+ # skipped. If both are false, show the file_collision menu, if the menu
83
+ # returns true, force it, otherwise skip.
84
+ #
85
+ def force_or_skip_or_conflict(force, skip, &block)
86
+ if force
87
+ say_status :force, :yellow
88
+ block.call unless pretend?
89
+ elsif skip
90
+ say_status :skip, :yellow
91
+ else
92
+ say_status :conflict, :red
93
+ force_or_skip_or_conflict(force_on_collision?, true, &block)
94
+ end
95
+ end
96
+
97
+ # Shows the file collision menu to the user and gets the result.
98
+ #
99
+ def force_on_collision?
100
+ base.shell.file_collision(destination){ render }
101
+ end
102
+
103
+ end
104
+ end
105
+ end
@@ -0,0 +1,57 @@
1
+ require File.expand_path('create_file', File.dirname(__FILE__))
2
+
3
+ module Ing
4
+ module Files
5
+
6
+ # Create a new file relative to the destination root from the given source.
7
+ #
8
+ # ==== Parameters
9
+ # destination<String>:: the relative path to the destination root.
10
+ # source<String|NilClass>:: the relative path to the source root.
11
+ # config<Hash>:: give :verbose => false to not log the status.
12
+ # :: give :symbolic => false for hard link.
13
+ #
14
+ # ==== Examples
15
+ #
16
+ # create_link "config/apache.conf", "/etc/apache.conf"
17
+ #
18
+ def create_link(destination, *args, &block)
19
+ config = args.last.is_a?(Hash) ? args.pop : {}
20
+ source = args.first
21
+ action CreateLink.new(self, destination, source, config)
22
+ end
23
+ alias :add_link :create_link
24
+
25
+ # CreateLink is a subset of CreateFile, which instead of taking a block of
26
+ # data, just takes a source string from the user.
27
+ #
28
+ class CreateLink < CreateFile #:nodoc:
29
+ attr_reader :data
30
+
31
+ # Checks if the content of the file at the destination is identical to the rendered result.
32
+ #
33
+ # ==== Returns
34
+ # Boolean:: true if it is identical, false otherwise.
35
+ #
36
+ def identical?
37
+ exists? && File.identical?(render, destination)
38
+ end
39
+
40
+ def invoke!
41
+ invoke_with_conflict_check do
42
+ FileUtils.mkdir_p(File.dirname(destination))
43
+ # Create a symlink by default
44
+ config[:symbolic] = true if config[:symbolic].nil?
45
+ File.unlink(destination) if exists?
46
+ if config[:symbolic]
47
+ File.symlink(render, destination)
48
+ else
49
+ File.link(render, destination)
50
+ end
51
+ end
52
+ given_destination
53
+ end
54
+
55
+ end
56
+ end
57
+ end
@@ -0,0 +1,98 @@
1
+ require File.expand_path('empty_directory', File.dirname(__FILE__))
2
+
3
+ module Ing
4
+ module Files
5
+ # Copies recursively the files from source directory to root directory.
6
+ # If any of the files finishes with .tt, it's considered to be a template
7
+ # and is placed in the destination without the extension .tt. If any
8
+ # empty directory is found, it's copied and all .empty_directory files are
9
+ # ignored. If any file name is wrapped within % signs, the text within
10
+ # the % signs will be executed as a method and replaced with the returned
11
+ # value. Let's suppose a doc directory with the following files:
12
+ #
13
+ # doc/
14
+ # components/.empty_directory
15
+ # README
16
+ # rdoc.rb.tt
17
+ # %app_name%.rb
18
+ #
19
+ # When invoked as:
20
+ #
21
+ # directory "doc"
22
+ #
23
+ # It will create a doc directory in the destination with the following
24
+ # files (assuming that the `app_name` method returns the value "blog"):
25
+ #
26
+ # doc/
27
+ # components/
28
+ # README
29
+ # rdoc.rb
30
+ # blog.rb
31
+ #
32
+ # <b>Encoded path note:</b> Since Thor internals use Object#respond_to? to check if it can
33
+ # expand %something%, this `something` should be a public method in the class calling
34
+ # #directory. If a method is private, Thor stack raises PrivateMethodEncodedError.
35
+ #
36
+ # ==== Parameters
37
+ # source<String>:: the relative path to the source root.
38
+ # destination<String>:: the relative path to the destination root.
39
+ # config<Hash>:: give :verbose => false to not log the status.
40
+ # If :recursive => false, does not look for paths recursively.
41
+ #
42
+ # ==== Examples
43
+ #
44
+ # directory "doc"
45
+ # directory "doc", "docs", :recursive => false
46
+ #
47
+ def directory(source, *args, &block)
48
+ config = args.last.is_a?(Hash) ? args.pop : {}
49
+ destination = args.first || source
50
+ action Directory.new(self, source, destination || source, config, &block)
51
+ end
52
+
53
+ class Directory < EmptyDirectory #:nodoc:
54
+ attr_reader :source
55
+
56
+ def initialize(base, source, destination=nil, config={}, &block)
57
+ @source = File.expand_path(base.find_in_source_paths(source.to_s))
58
+ @block = block
59
+ super(base, destination, { :recursive => true }.merge(config))
60
+ end
61
+
62
+ def invoke!
63
+ base.empty_directory given_destination, config
64
+ execute!
65
+ end
66
+
67
+ def revoke!
68
+ execute!
69
+ end
70
+
71
+ protected
72
+
73
+ def execute!
74
+ lookup = Util.escape_globs(source)
75
+ lookup = config[:recursive] ? File.join(lookup, '**') : lookup
76
+ lookup = File.join(lookup, '{*,.[a-z]*}')
77
+
78
+ Dir[lookup].sort.each do |file_source|
79
+ next if File.directory?(file_source)
80
+ file_destination = File.join(given_destination, file_source.gsub(source, '.'))
81
+ file_destination.gsub!('/./', '/')
82
+
83
+ case file_source
84
+ when /\.empty_directory$/
85
+ dirname = File.dirname(file_destination).gsub(/\/\.$/, '')
86
+ next if dirname == given_destination
87
+ base.empty_directory(dirname, config)
88
+ when /\.tt$/
89
+ destination = base.template(file_source, file_destination[0..-4], config, &@block)
90
+ else
91
+ destination = base.copy_file(file_source, file_destination, config, &@block)
92
+ end
93
+ end
94
+ end
95
+
96
+ end
97
+ end
98
+ end
@@ -0,0 +1,155 @@
1
+ module Ing
2
+ module Files
3
+
4
+ # Creates an empty directory.
5
+ #
6
+ # ==== Parameters
7
+ # destination<String>:: the relative path to the destination root.
8
+ # config<Hash>:: give :verbose => false to not log the status.
9
+ #
10
+ # ==== Examples
11
+ #
12
+ # empty_directory "doc"
13
+ #
14
+ def empty_directory(destination, config={})
15
+ action EmptyDirectory.new(self, destination, config)
16
+ end
17
+
18
+ # Class which holds create directory logic. This is the base class for
19
+ # other actions like create_file and directory.
20
+ #
21
+ # This implementation is based in Templater actions, created by Jonas Nicklas
22
+ # and Michael S. Klishin under MIT LICENSE.
23
+ #
24
+ class EmptyDirectory #:nodoc:
25
+ attr_reader :base, :destination, :given_destination, :relative_destination, :config
26
+
27
+ # Initializes given the source and destination.
28
+ #
29
+ # ==== Parameters
30
+ # base<Thor::Base>:: A Thor::Base instance
31
+ # source<String>:: Relative path to the source of this file
32
+ # destination<String>:: Relative path to the destination of this file
33
+ # config<Hash>:: give :verbose => false to not log the status.
34
+ #
35
+ def initialize(base, destination, config={})
36
+ @base, @config = base, { :verbose => true }.merge(config)
37
+ self.destination = destination
38
+ end
39
+
40
+ # Checks if the destination file already exists.
41
+ #
42
+ # ==== Returns
43
+ # Boolean:: true if the file exists, false otherwise.
44
+ #
45
+ def exists?
46
+ ::File.exists?(destination)
47
+ end
48
+
49
+ def invoke!
50
+ invoke_with_conflict_check do
51
+ ::FileUtils.mkdir_p(destination)
52
+ end
53
+ end
54
+
55
+ def revoke!
56
+ say_status :remove, :red
57
+ ::FileUtils.rm_rf(destination) if !pretend? && exists?
58
+ given_destination
59
+ end
60
+
61
+ protected
62
+
63
+ # Shortcut for pretend.
64
+ #
65
+ def pretend?
66
+ base.options[:pretend]
67
+ end
68
+
69
+ # Sets the absolute destination value from a relative destination value.
70
+ # It also stores the given and relative destination. Let's suppose our
71
+ # script is being executed on "dest", it sets the destination root to
72
+ # "dest". The destination, given_destination and relative_destination
73
+ # are related in the following way:
74
+ #
75
+ # inside "bar" do
76
+ # empty_directory "baz"
77
+ # end
78
+ #
79
+ # destination #=> dest/bar/baz
80
+ # relative_destination #=> bar/baz
81
+ # given_destination #=> baz
82
+ #
83
+ def destination=(destination)
84
+ if destination
85
+ @given_destination = convert_encoded_instructions(destination.to_s)
86
+ @destination = ::File.expand_path(@given_destination, base.current_destination)
87
+ @relative_destination = base.relative_to_original_destination_root(@destination)
88
+ end
89
+ end
90
+
91
+ # Filenames in the encoded form are converted. If you have a file:
92
+ #
93
+ # %file_name%.rb
94
+ #
95
+ # It calls #file_name from the base and replaces %-string with the
96
+ # return value (should be String) of #file_name:
97
+ #
98
+ # user.rb
99
+ #
100
+ # The method referenced by %-string SHOULD be public. Otherwise you
101
+ # get the exception with the corresponding error message.
102
+ #
103
+ def convert_encoded_instructions(filename)
104
+ filename.gsub(/%(.*?)%/) do |initial_string|
105
+ call_public_method($1.strip) or initial_string
106
+ end
107
+ end
108
+
109
+ #TODO: base.public_send(sym) rescue nil instead?
110
+
111
+ # Calls `base`'s public method `sym`.
112
+ # Returns:: result of `base.sym` or `nil` if `sym` wasn't found in
113
+ # `base`
114
+ # Raises:: NoMethodError if `sym` references
115
+ # a private method.
116
+ def call_public_method(sym)
117
+ if base.respond_to?(sym)
118
+ base.send(sym)
119
+ elsif base.respond_to?(sym, true)
120
+ raise NoMethodError,
121
+ "Method #{base.class}##{sym} should be public, not private"
122
+ else
123
+ nil
124
+ end
125
+ end
126
+
127
+ # Receives a hash of options and just execute the block if some
128
+ # conditions are met.
129
+ #
130
+ def invoke_with_conflict_check(&block)
131
+ if exists?
132
+ on_conflict_behavior(&block)
133
+ else
134
+ say_status :create, :green
135
+ block.call unless pretend?
136
+ end
137
+
138
+ destination
139
+ end
140
+
141
+ # What to do when the destination file already exists.
142
+ #
143
+ def on_conflict_behavior(&block)
144
+ say_status :exist, :blue
145
+ end
146
+
147
+ # Shortcut to say_status shell method.
148
+ #
149
+ def say_status(status, color)
150
+ base.shell.say_status status, relative_destination, color if config[:verbose]
151
+ end
152
+
153
+ end
154
+ end
155
+ end