hanami-cli 2.1.0.beta1 → 2.1.0.rc1

Sign up to get free protection for your applications and to get access to all the features.
Files changed (56) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +37 -0
  3. data/Gemfile +5 -2
  4. data/hanami-cli.gemspec +1 -1
  5. data/lib/hanami/cli/bundler.rb +19 -5
  6. data/lib/hanami/cli/command.rb +4 -4
  7. data/lib/hanami/cli/commands/app/assets/command.rb +69 -0
  8. data/lib/hanami/cli/commands/app/assets/compile.rb +32 -0
  9. data/lib/hanami/cli/commands/app/assets/watch.rb +32 -0
  10. data/lib/hanami/cli/commands/app/assets.rb +16 -0
  11. data/lib/hanami/cli/commands/app/command.rb +2 -2
  12. data/lib/hanami/cli/commands/app/dev.rb +45 -0
  13. data/lib/hanami/cli/commands/app/generate/action.rb +3 -2
  14. data/lib/hanami/cli/commands/app/generate/part.rb +49 -0
  15. data/lib/hanami/cli/commands/app/install.rb +16 -1
  16. data/lib/hanami/cli/commands/app.rb +9 -0
  17. data/lib/hanami/cli/commands/gem/new.rb +57 -7
  18. data/lib/hanami/cli/errors.rb +30 -0
  19. data/lib/hanami/cli/files.rb +8 -2
  20. data/lib/hanami/cli/generators/app/action/action.erb +5 -1
  21. data/lib/hanami/cli/generators/app/action/slice_action.erb +5 -1
  22. data/lib/hanami/cli/generators/app/action.rb +49 -12
  23. data/lib/hanami/cli/generators/app/part/app_base_part.erb +9 -0
  24. data/lib/hanami/cli/generators/app/part/app_part.erb +13 -0
  25. data/lib/hanami/cli/generators/app/part/slice_base_part.erb +9 -0
  26. data/lib/hanami/cli/generators/app/part/slice_part.erb +13 -0
  27. data/lib/hanami/cli/generators/app/part.rb +101 -0
  28. data/lib/hanami/cli/generators/app/part_context.rb +98 -0
  29. data/lib/hanami/cli/generators/app/slice/app_css.erb +5 -0
  30. data/lib/hanami/cli/generators/app/slice/app_js.erb +1 -0
  31. data/lib/hanami/cli/generators/app/slice/app_layout.erb +18 -0
  32. data/lib/hanami/cli/generators/app/slice.rb +9 -3
  33. data/lib/hanami/cli/generators/app/slice_context.rb +18 -0
  34. data/lib/hanami/cli/generators/context.rb +70 -3
  35. data/lib/hanami/cli/generators/gem/app/404.html +76 -5
  36. data/lib/hanami/cli/generators/gem/app/500.html +76 -5
  37. data/lib/hanami/cli/generators/gem/app/app_css.erb +5 -0
  38. data/lib/hanami/cli/generators/gem/app/app_js.erb +1 -0
  39. data/lib/hanami/cli/generators/gem/app/app_layout.erb +18 -0
  40. data/lib/hanami/cli/generators/gem/app/assets.mjs +14 -0
  41. data/lib/hanami/cli/generators/gem/app/dev +8 -0
  42. data/lib/hanami/cli/generators/gem/app/favicon.ico +0 -0
  43. data/lib/hanami/cli/generators/gem/app/gemfile.erb +14 -8
  44. data/lib/hanami/cli/generators/gem/app/gitignore.erb +4 -0
  45. data/lib/hanami/cli/generators/gem/app/package.json.erb +10 -0
  46. data/lib/hanami/cli/generators/gem/app/procfile.erb +4 -0
  47. data/lib/hanami/cli/generators/gem/app/puma.erb +37 -7
  48. data/lib/hanami/cli/generators/gem/app/routes.erb +1 -1
  49. data/lib/hanami/cli/generators/gem/app.rb +21 -4
  50. data/lib/hanami/cli/generators/version.rb +12 -0
  51. data/lib/hanami/cli/interactive_system_call.rb +64 -0
  52. data/lib/hanami/cli/system_call.rb +8 -2
  53. data/lib/hanami/cli/version.rb +1 -1
  54. metadata +28 -6
  55. data/lib/hanami/cli/generators/app/slice/layouts_app.html.erb +0 -1
  56. data/lib/hanami/cli/generators/gem/app/layouts_app.html.erb +0 -1
@@ -2,60 +2,90 @@
2
2
 
3
3
  module Hanami
4
4
  module CLI
5
+ # @since 0.1.0
6
+ # @api public
5
7
  class Error < StandardError
6
8
  end
7
9
 
10
+ # @since 2.0.0
11
+ # @api public
8
12
  class NotImplementedError < Error
9
13
  end
10
14
 
15
+ # @since 2.0.0
16
+ # @api public
11
17
  class BundleInstallError < Error
12
18
  def initialize(message)
13
19
  super("`bundle install' failed\n\n\n#{message.inspect}")
14
20
  end
15
21
  end
16
22
 
23
+ # @since 2.0.0
24
+ # @api public
17
25
  class HanamiInstallError < Error
18
26
  def initialize(message)
19
27
  super("`hanami install' failed\n\n\n#{message.inspect}")
20
28
  end
21
29
  end
22
30
 
31
+ # @since 2.1.0
32
+ # @api public
33
+ class HanamiExecError < Error
34
+ def initialize(cmd, message)
35
+ super("`bundle exec hanami #{cmd}' failed\n\n\n#{message.inspect}")
36
+ end
37
+ end
38
+
39
+ # @since 2.0.0
40
+ # @api public
23
41
  class PathAlreadyExistsError < Error
24
42
  def initialize(path)
25
43
  super("Cannot create new Hanami app in an existing path: `#{path}'")
26
44
  end
27
45
  end
28
46
 
47
+ # @since 2.0.0
48
+ # @api public
29
49
  class MissingSliceError < Error
30
50
  def initialize(slice)
31
51
  super("slice `#{slice}' is missing, please generate with `hanami generate slice #{slice}'")
32
52
  end
33
53
  end
34
54
 
55
+ # @since 2.0.0
56
+ # @api public
35
57
  class InvalidURLError < Error
36
58
  def initialize(url)
37
59
  super("invalid URL: `#{url}'")
38
60
  end
39
61
  end
40
62
 
63
+ # @since 2.0.0
64
+ # @api public
41
65
  class InvalidURLPrefixError < Error
42
66
  def initialize(url)
43
67
  super("invalid URL prefix: `#{url}'")
44
68
  end
45
69
  end
46
70
 
71
+ # @since 2.0.0
72
+ # @api public
47
73
  class InvalidActionNameError < Error
48
74
  def initialize(name)
49
75
  super("cannot parse controller and action name: `#{name}'\n\texample: `hanami generate action users.show'")
50
76
  end
51
77
  end
52
78
 
79
+ # @since 2.0.0
80
+ # @api public
53
81
  class UnknownHTTPMethodError < Error
54
82
  def initialize(name)
55
83
  super("unknown HTTP method: `#{name}'")
56
84
  end
57
85
  end
58
86
 
87
+ # @since 2.0.0
88
+ # @api public
59
89
  class UnsupportedDatabaseSchemeError < Error
60
90
  def initialize(scheme)
61
91
  super("`#{scheme}' is not a supported db scheme")
@@ -1,5 +1,7 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ require "dry/files"
4
+
3
5
  module Hanami
4
6
  module CLI
5
7
  # @since 2.0.0
@@ -29,7 +31,7 @@ module Hanami
29
31
  def mkdir(path)
30
32
  unless exist?(path)
31
33
  super
32
- created("#{path}/")
34
+ created(_path(path))
33
35
  end
34
36
  end
35
37
 
@@ -53,7 +55,11 @@ module Hanami
53
55
  end
54
56
 
55
57
  def within_folder(path)
56
- out.puts "-> Within #{path}/"
58
+ out.puts "-> Within #{_path(path)}"
59
+ end
60
+
61
+ def _path(path)
62
+ path + ::File::SEPARATOR
57
63
  end
58
64
  end
59
65
  end
@@ -4,8 +4,12 @@ module <%= camelized_app_name %>
4
4
  module Actions
5
5
  <%= module_controller_declaration %>
6
6
  <%= module_controller_offset %>class <%= camelized_action_name %> < <%= camelized_app_name %>::Action
7
- <%= module_controller_offset %> def handle(*, response)
7
+ <%- if bundled_views? -%>
8
+ <%= module_controller_offset %> def handle(request, response)
9
+ <%- else -%>
10
+ <%= module_controller_offset %> def handle(request, response)
8
11
  <%= module_controller_offset %> response.body = self.class.name
12
+ <%- end -%>
9
13
  <%= module_controller_offset %> end
10
14
  <%= module_controller_offset %>end
11
15
  <%= module_controller_end %>
@@ -4,8 +4,12 @@ module <%= camelized_slice_name %>
4
4
  module Actions
5
5
  <%= module_controller_declaration %>
6
6
  <%= module_controller_offset %>class <%= camelized_action_name %> < <%= camelized_slice_name %>::Action
7
- <%= module_controller_offset %> def handle(*, response)
7
+ <%- if bundled_views? -%>
8
+ <%= module_controller_offset %> def handle(request, response)
9
+ <%- else -%>
10
+ <%= module_controller_offset %> def handle(request, response)
8
11
  <%= module_controller_offset %> response.body = self.class.name
12
+ <%- end -%>
9
13
  <%= module_controller_offset %> end
10
14
  <%= module_controller_offset %>end
11
15
  <%= module_controller_end %>
@@ -19,11 +19,10 @@ module Hanami
19
19
  @inflector = inflector
20
20
  end
21
21
 
22
- # rubocop:disable Layout/LineLength
23
-
24
22
  # @since 2.0.0
25
23
  # @api private
26
- def call(app, controller, action, url, http, format, skip_view, slice, context: ActionContext.new(inflector, app, slice, controller, action))
24
+ def call(app, controller, action, url, http, format, skip_view, slice, context: nil)
25
+ context ||= ActionContext.new(inflector, app, slice, controller, action)
27
26
  if slice
28
27
  generate_for_slice(controller, action, url, http, format, skip_view, slice, context)
29
28
  else
@@ -31,8 +30,6 @@ module Hanami
31
30
  end
32
31
  end
33
32
 
34
- # rubocop:enable Layout/LineLength
35
-
36
33
  private
37
34
 
38
35
  ROUTE_HTTP_METHODS = %w[get post delete put patch trace options link unlink].freeze
@@ -59,6 +56,14 @@ module Hanami
59
56
  }.freeze
60
57
  private_constant :ROUTE_RESTFUL_URL_SUFFIXES
61
58
 
59
+ # @api private
60
+ # @since 2.1.0
61
+ RESTFUL_COUNTERPART_VIEWS = {
62
+ "create" => "new",
63
+ "update" => "edit"
64
+ }.freeze
65
+ private_constant :RESTFUL_COUNTERPART_VIEWS
66
+
62
67
  PATH_SEPARATOR = "/"
63
68
  private_constant :PATH_SEPARATOR
64
69
 
@@ -80,7 +85,7 @@ module Hanami
80
85
  fs.mkdir(directory = fs.join(slice_directory, "actions", controller))
81
86
  fs.write(fs.join(directory, "#{action}.rb"), t("slice_action.erb", context))
82
87
 
83
- unless skip_view
88
+ if generate_view?(skip_view, action, directory)
84
89
  fs.mkdir(directory = fs.join(slice_directory, "views", controller))
85
90
  fs.write(fs.join(directory, "#{action}.rb"), t("slice_view.erb", context))
86
91
 
@@ -100,12 +105,15 @@ module Hanami
100
105
  fs.mkdir(directory = fs.join("app", "actions", controller))
101
106
  fs.write(fs.join(directory, "#{action}.rb"), t("action.erb", context))
102
107
 
103
- unless skip_view
104
- fs.mkdir(directory = fs.join("app", "views", controller))
105
- fs.write(fs.join(directory, "#{action}.rb"), t("view.erb", context))
108
+ view = action
109
+ view_directory = fs.join("app", "views", controller)
106
110
 
107
- fs.mkdir(directory = fs.join("app", "templates", controller))
108
- fs.write(fs.join(directory, "#{action}.#{format}.erb"),
111
+ if generate_view?(skip_view, view, view_directory)
112
+ fs.mkdir(view_directory)
113
+ fs.write(fs.join(view_directory, "#{view}.rb"), t("view.erb", context))
114
+
115
+ fs.mkdir(template_directory = fs.join("app", "templates", controller))
116
+ fs.write(fs.join(template_directory, "#{view}.#{format}.erb"),
109
117
  t(template_with_format_ext("template", format), context))
110
118
  end
111
119
  end
@@ -120,6 +128,35 @@ module Hanami
120
128
  http)} "#{route_url(controller, action, url)}", to: "#{controller.join('.')}.#{action}")
121
129
  end
122
130
 
131
+ # @api private
132
+ # @since 2.1.0
133
+ def generate_view?(skip_view, view, directory)
134
+ return false if skip_view
135
+ return generate_restful_view?(view, directory) if rest_view?(view)
136
+
137
+ true
138
+ end
139
+
140
+ # @api private
141
+ # @since 2.1.0
142
+ def generate_restful_view?(view, directory)
143
+ corresponding_action = corresponding_restful_view(view)
144
+
145
+ !fs.exist?(fs.join(directory, "#{corresponding_action}.rb"))
146
+ end
147
+
148
+ # @api private
149
+ # @since 2.1.0
150
+ def rest_view?(view)
151
+ RESTFUL_COUNTERPART_VIEWS.keys.include?(view)
152
+ end
153
+
154
+ # @api private
155
+ # @since 2.1.0
156
+ def corresponding_restful_view(view)
157
+ RESTFUL_COUNTERPART_VIEWS.fetch(view, nil)
158
+ end
159
+
123
160
  def template_with_format_ext(name, format)
124
161
  ext =
125
162
  case format.to_sym
@@ -136,7 +173,7 @@ module Hanami
136
173
  require "erb"
137
174
 
138
175
  ERB.new(
139
- File.read(__dir__ + "/action/#{path}")
176
+ File.read(__dir__ + "/action/#{path}"), trim_mode: "-",
140
177
  ).result(context.ctx)
141
178
  end
142
179
 
@@ -0,0 +1,9 @@
1
+ # auto_register: false
2
+ # frozen_string_literal: true
3
+
4
+ module <%= camelized_app_name %>
5
+ module Views
6
+ class Part < Hanami::View::Part
7
+ end
8
+ end
9
+ end
@@ -0,0 +1,13 @@
1
+ # auto_register: false
2
+ # frozen_string_literal: true
3
+
4
+ module <%= camelized_app_name %>
5
+ module Views
6
+ module Parts
7
+ <%= module_namespace_declaration -%>
8
+ <%= module_namespace_offset %>class <%= camelized_name %> < <%= camelized_app_name %>::Views::Part
9
+ <%= module_namespace_offset %>end
10
+ <%= module_namespace_end -%>
11
+ end
12
+ end
13
+ end
@@ -0,0 +1,9 @@
1
+ # auto_register: false
2
+ # frozen_string_literal: true
3
+
4
+ module <%= camelized_slice_name %>
5
+ module Views
6
+ class Part < <%= camelized_app_name %>::Views::Part
7
+ end
8
+ end
9
+ end
@@ -0,0 +1,13 @@
1
+ # auto_register: false
2
+ # frozen_string_literal: true
3
+
4
+ module <%= camelized_slice_name %>
5
+ module Views
6
+ module Parts
7
+ <%= module_namespace_declaration -%>
8
+ <%= module_namespace_offset %>class <%= camelized_name %> < <%= camelized_slice_name %>::Views::Part
9
+ <%= module_namespace_offset %>end
10
+ <%= module_namespace_end -%>
11
+ end
12
+ end
13
+ end
@@ -0,0 +1,101 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "erb"
4
+ require "dry/files"
5
+ require_relative "../../errors"
6
+
7
+ module Hanami
8
+ module CLI
9
+ module Generators
10
+ module App
11
+ # @since 2.1.0
12
+ # @api private
13
+ class Part
14
+ # @since 2.1.0
15
+ # @api private
16
+ def initialize(fs:, inflector:)
17
+ @fs = fs
18
+ @inflector = inflector
19
+ end
20
+
21
+ # @since 2.1.0
22
+ # @api private
23
+ def call(app, key, slice)
24
+ context = PartContext.new(inflector, app, slice, key)
25
+
26
+ if slice
27
+ generate_for_slice(context, slice)
28
+ else
29
+ generate_for_app(context)
30
+ end
31
+ end
32
+
33
+ private
34
+
35
+ # @since 2.1.0
36
+ # @api private
37
+ attr_reader :fs
38
+
39
+ # @since 2.1.0
40
+ # @api private
41
+ attr_reader :inflector
42
+
43
+ # @since 2.1.0
44
+ # @api private
45
+ def generate_for_slice(context, slice)
46
+ slice_directory = fs.join("slices", slice)
47
+ raise MissingSliceError.new(slice) unless fs.directory?(slice_directory)
48
+
49
+ generate_base_part_for_app(context)
50
+ generate_base_part_for_slice(context, slice)
51
+
52
+ fs.mkdir(directory = fs.join(slice_directory, "views", "parts", *context.underscored_namespace))
53
+ fs.write(fs.join(directory, "#{context.underscored_name}.rb"), t("slice_part.erb", context))
54
+ end
55
+
56
+ # @since 2.1.0
57
+ # @api private
58
+ def generate_for_app(context)
59
+ generate_base_part_for_app(context)
60
+
61
+ fs.mkdir(directory = fs.join("app", "views", "parts", *context.underscored_namespace))
62
+ fs.write(fs.join(directory, "#{context.underscored_name}.rb"), t("app_part.erb", context))
63
+ end
64
+
65
+ # @since 2.1.0
66
+ # @api private
67
+ def generate_base_part_for_app(context)
68
+ path = fs.join("app", "views", "part.rb")
69
+ return if fs.exist?(path)
70
+
71
+ fs.write(path, t("app_base_part.erb", context))
72
+ end
73
+
74
+ # @since 2.1.0
75
+ # @api private
76
+ def generate_base_part_for_slice(context, slice)
77
+ path = fs.join("slices", slice, "views", "part.rb")
78
+ return if fs.exist?(path)
79
+
80
+ fs.write(path, t("slice_base_part.erb", context))
81
+ end
82
+
83
+ # @since 2.1.0
84
+ # @api private
85
+ def template(path, context)
86
+ require "erb"
87
+
88
+ ERB.new(
89
+ File.read(__dir__ + "/part/#{path}"),
90
+ trim_mode: "-"
91
+ ).result(context.ctx)
92
+ end
93
+
94
+ # @since 2.1.0
95
+ # @api private
96
+ alias_method :t, :template
97
+ end
98
+ end
99
+ end
100
+ end
101
+ end
@@ -0,0 +1,98 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "slice_context"
4
+ require "dry/files/path"
5
+
6
+ module Hanami
7
+ module CLI
8
+ module Generators
9
+ # @since 2.1.0
10
+ # @api private
11
+ module App
12
+ # @since 2.1.0
13
+ # @api private
14
+ class PartContext < SliceContext
15
+ # TODO: move these constants somewhere that will let us reuse them
16
+
17
+ # @since 2.1.0
18
+ # @api private
19
+ KEY_SEPARATOR = "."
20
+ private_constant :KEY_SEPARATOR
21
+
22
+ # @since 2.1.0
23
+ # @api private
24
+ INDENTATION = " "
25
+ private_constant :INDENTATION
26
+
27
+ # @since 2.1.0
28
+ # @api private
29
+ OFFSET = INDENTATION * 2
30
+ private_constant :OFFSET
31
+
32
+ # @since 2.1.0
33
+ # @api private
34
+ attr_reader :key
35
+
36
+ # @since 2.1.0
37
+ # @api private
38
+ def initialize(inflector, app, slice, key)
39
+ @key = key
40
+ super(inflector, app, slice, nil)
41
+ end
42
+
43
+ # @since 2.1.0
44
+ # @api private
45
+ def namespaces
46
+ @namespaces ||= key.split(KEY_SEPARATOR)[..-2]
47
+ end
48
+
49
+ # @since 2.1.0
50
+ # @api private
51
+ def name
52
+ @name ||= key.split(KEY_SEPARATOR)[-1]
53
+ end
54
+
55
+ # @since 2.1.0
56
+ # @api private
57
+ def camelized_name
58
+ inflector.camelize(name)
59
+ end
60
+
61
+ # @since 2.1.0
62
+ # @api private
63
+ def underscored_namespace
64
+ namespaces.map { inflector.underscore(_1) }
65
+ end
66
+
67
+ # @since 2.1.0
68
+ # @api private
69
+ def underscored_name
70
+ inflector.underscore(name)
71
+ end
72
+
73
+ # @since 2.1.0
74
+ # @api private
75
+ def module_namespace_declaration
76
+ namespaces.each_with_index.map { |token, i|
77
+ "#{OFFSET}#{INDENTATION * i}module #{inflector.camelize(token)}"
78
+ }.join($/)
79
+ end
80
+
81
+ # @since 2.1.0
82
+ # @api private
83
+ def module_namespace_end
84
+ namespaces.each_with_index.map { |_, i|
85
+ "#{OFFSET}#{INDENTATION * i}end"
86
+ }.reverse.join($/)
87
+ end
88
+
89
+ # @since 2.1.0
90
+ # @api private
91
+ def module_namespace_offset
92
+ "#{OFFSET}#{INDENTATION * namespaces.count}"
93
+ end
94
+ end
95
+ end
96
+ end
97
+ end
98
+ end
@@ -0,0 +1,5 @@
1
+ body {
2
+ background-color: #fff;
3
+ color: #000;
4
+ font-family: sans-serif;
5
+ }
@@ -0,0 +1 @@
1
+ import "../css/app.css";
@@ -0,0 +1,18 @@
1
+ <!DOCTYPE html>
2
+ <html lang="en">
3
+ <head>
4
+ <meta charset="UTF-8">
5
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
6
+ <title><%= humanized_app_name %> - <%= humanized_slice_name %></title>
7
+ <%- if bundled_assets? -%>
8
+ <%%= favicon %>
9
+ <%= stylesheet_erb_tag %>
10
+ <%- end -%>
11
+ </head>
12
+ <body>
13
+ <%%= yield %>
14
+ <%- if bundled_assets? -%>
15
+ <%= javascript_erb_tag %>
16
+ <%- end -%>
17
+ </body>
18
+ </html>
@@ -30,8 +30,13 @@ module Hanami
30
30
  fs.write(fs.join(directory, "action.rb"), t("action.erb", context))
31
31
  fs.write(fs.join(directory, "view.rb"), t("view.erb", context))
32
32
  fs.write(fs.join(directory, "views", "helpers.rb"), t("helpers.erb", context))
33
- fs.write(fs.join(directory, "templates", "layouts", "app.html.erb"),
34
- File.read(File.join(__dir__, "slice", "layouts_app.html.erb")))
33
+ fs.write(fs.join(directory, "templates", "layouts", "app.html.erb"), t("app_layout.erb", context))
34
+
35
+ if context.bundled_assets?
36
+ fs.write(fs.join(directory, "assets", "js", "app.js"), t("app_js.erb", context))
37
+ fs.write(fs.join(directory, "assets", "css", "app.css"), t("app_css.erb", context))
38
+ end
39
+
35
40
  # fs.write(fs.join(directory, "/entities.rb"), t("entities.erb", context))
36
41
  # fs.write(fs.join(directory, "/repository.rb"), t("repository.erb", context))
37
42
 
@@ -53,7 +58,8 @@ module Hanami
53
58
  require "erb"
54
59
 
55
60
  ERB.new(
56
- File.read(__dir__ + "/slice/#{path}")
61
+ File.read(__dir__ + "/slice/#{path}"),
62
+ trim_mode: "-"
57
63
  ).result(context.ctx)
58
64
  end
59
65
 
@@ -29,6 +29,24 @@ module Hanami
29
29
  inflector.underscore(slice)
30
30
  end
31
31
 
32
+ # @since 2.1.0
33
+ # @api private
34
+ def humanized_slice_name
35
+ inflector.humanize(slice)
36
+ end
37
+
38
+ # @since 2.1.0
39
+ # @api private
40
+ def stylesheet_erb_tag
41
+ %(<%= css "#{slice}/app" %>)
42
+ end
43
+
44
+ # @since 2.1.0
45
+ # @api private
46
+ def javascript_erb_tag
47
+ %(<%= js "#{slice}/app" %>)
48
+ end
49
+
32
50
  private
33
51
 
34
52
  attr_reader :slice