haveapi-fs 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (89) hide show
  1. checksums.yaml +7 -0
  2. data/.yardopts +1 -0
  3. data/CHANGELOG +2 -0
  4. data/Gemfile +3 -0
  5. data/LICENSE.txt +21 -0
  6. data/README.md +338 -0
  7. data/Rakefile +1 -0
  8. data/assets/css/bootstrap.min.css +6 -0
  9. data/assets/css/local.css +11 -0
  10. data/bin/haveapi-fs +5 -0
  11. data/haveapi-fs.gemspec +30 -0
  12. data/lib/core_ext/string.rb +9 -0
  13. data/lib/haveapi/fs/auth/base.rb +56 -0
  14. data/lib/haveapi/fs/auth/basic.rb +29 -0
  15. data/lib/haveapi/fs/auth/noauth.rb +9 -0
  16. data/lib/haveapi/fs/auth/token.rb +39 -0
  17. data/lib/haveapi/fs/cache.rb +71 -0
  18. data/lib/haveapi/fs/cleaner.rb +56 -0
  19. data/lib/haveapi/fs/component.rb +237 -0
  20. data/lib/haveapi/fs/components/action_dir.rb +106 -0
  21. data/lib/haveapi/fs/components/action_errors.rb +49 -0
  22. data/lib/haveapi/fs/components/action_exec.rb +12 -0
  23. data/lib/haveapi/fs/components/action_exec_edit.rb +90 -0
  24. data/lib/haveapi/fs/components/action_input.rb +40 -0
  25. data/lib/haveapi/fs/components/action_message.rb +19 -0
  26. data/lib/haveapi/fs/components/action_output.rb +79 -0
  27. data/lib/haveapi/fs/components/action_status.rb +32 -0
  28. data/lib/haveapi/fs/components/cache_stats.rb +19 -0
  29. data/lib/haveapi/fs/components/component_list.rb +24 -0
  30. data/lib/haveapi/fs/components/create_action_dir.rb +15 -0
  31. data/lib/haveapi/fs/components/delete_action_dir.rb +22 -0
  32. data/lib/haveapi/fs/components/directory.rb +45 -0
  33. data/lib/haveapi/fs/components/directory_reset.rb +8 -0
  34. data/lib/haveapi/fs/components/executable.rb +75 -0
  35. data/lib/haveapi/fs/components/file.rb +43 -0
  36. data/lib/haveapi/fs/components/groff_help_file.rb +9 -0
  37. data/lib/haveapi/fs/components/help_file.rb +28 -0
  38. data/lib/haveapi/fs/components/html_help_file.rb +24 -0
  39. data/lib/haveapi/fs/components/index_filter.rb +63 -0
  40. data/lib/haveapi/fs/components/info_files.rb +19 -0
  41. data/lib/haveapi/fs/components/instance_create.rb +20 -0
  42. data/lib/haveapi/fs/components/instance_edit.rb +49 -0
  43. data/lib/haveapi/fs/components/list_item.rb +28 -0
  44. data/lib/haveapi/fs/components/md_help_file.rb +24 -0
  45. data/lib/haveapi/fs/components/meta_dir.rb +42 -0
  46. data/lib/haveapi/fs/components/meta_file.rb +21 -0
  47. data/lib/haveapi/fs/components/parameter.rb +132 -0
  48. data/lib/haveapi/fs/components/pry.rb +9 -0
  49. data/lib/haveapi/fs/components/remote_control_file.rb +92 -0
  50. data/lib/haveapi/fs/components/resource_action_dir.rb +72 -0
  51. data/lib/haveapi/fs/components/resource_dir.rb +161 -0
  52. data/lib/haveapi/fs/components/resource_id.rb +15 -0
  53. data/lib/haveapi/fs/components/resource_instance_dir.rb +146 -0
  54. data/lib/haveapi/fs/components/rfuse_check.rb +3 -0
  55. data/lib/haveapi/fs/components/root.rb +75 -0
  56. data/lib/haveapi/fs/components/save_instance.rb +11 -0
  57. data/lib/haveapi/fs/components/unsaved_list.rb +24 -0
  58. data/lib/haveapi/fs/components/update_action_dir.rb +31 -0
  59. data/lib/haveapi/fs/context.rb +43 -0
  60. data/lib/haveapi/fs/exceptions.rb +0 -0
  61. data/lib/haveapi/fs/factory.rb +59 -0
  62. data/lib/haveapi/fs/fs.rb +198 -0
  63. data/lib/haveapi/fs/help.rb +91 -0
  64. data/lib/haveapi/fs/main.rb +134 -0
  65. data/lib/haveapi/fs/remote_control.rb +29 -0
  66. data/lib/haveapi/fs/version.rb +5 -0
  67. data/lib/haveapi/fs/worker.rb +77 -0
  68. data/lib/haveapi/fs.rb +65 -0
  69. data/templates/help/html/action_dir.erb +33 -0
  70. data/templates/help/html/action_errors.erb +4 -0
  71. data/templates/help/html/action_input.erb +40 -0
  72. data/templates/help/html/action_output.erb +21 -0
  73. data/templates/help/html/index_filter.erb +16 -0
  74. data/templates/help/html/layout.erb +45 -0
  75. data/templates/help/html/resource_action_dir.erb +18 -0
  76. data/templates/help/html/resource_dir.erb +18 -0
  77. data/templates/help/html/resource_instance_dir.erb +64 -0
  78. data/templates/help/html/root.erb +42 -0
  79. data/templates/help/md/action_dir.erb +29 -0
  80. data/templates/help/md/action_errors.erb +2 -0
  81. data/templates/help/md/action_input.erb +23 -0
  82. data/templates/help/md/action_output.erb +11 -0
  83. data/templates/help/md/index_filter.erb +11 -0
  84. data/templates/help/md/layout.erb +14 -0
  85. data/templates/help/md/resource_action_dir.erb +10 -0
  86. data/templates/help/md/resource_dir.erb +15 -0
  87. data/templates/help/md/resource_instance_dir.erb +42 -0
  88. data/templates/help/md/root.erb +34 -0
  89. metadata +231 -0
@@ -0,0 +1,237 @@
1
+ module HaveAPI::Fs
2
+ # All built-in components are stored in this module.
3
+ module Components ; end
4
+
5
+ # The basic building block of the file system. Every directory and file is
6
+ # represented by a subclass of this class.
7
+ class Component
8
+ # An encapsulation of a Hash to store child components.
9
+ class Children
10
+ attr_accessor :context
11
+
12
+ # @param [HaveAPI::Fs::Context] ctx
13
+ def initialize(ctx)
14
+ @context = ctx
15
+ @store = {}
16
+ end
17
+
18
+ def [](k)
19
+ @store[k]
20
+ end
21
+
22
+ # Replace a child named `k` by a new child represented by `v`. The old
23
+ # child, if present, is invalidated and dropped from the cache.
24
+ # {Factory} is used to create an instance of `v`.
25
+ #
26
+ # @param [Symbol] k
27
+ # @param [Array] v
28
+ def []=(k, v)
29
+ if @store.has_key?(k)
30
+ @store[k].invalidate
31
+ @store[k].context.cache.drop_below(@store[k].path)
32
+ end
33
+
34
+ @store[k] = Factory.create(@context, k, *v)
35
+ end
36
+
37
+ def set(k, v)
38
+ @store[k] = v
39
+ end
40
+
41
+ %i(has_key? clear each select detect delete_if).each do |m|
42
+ define_method(m) do |*args, &block|
43
+ @store.send(m, *args, &block)
44
+ end
45
+ end
46
+ end
47
+
48
+ class << self
49
+ # Define reader methods for child components.
50
+ def children_reader(*args)
51
+ args.each do |arg|
52
+ define_method(arg) { children[arg] }
53
+ end
54
+ end
55
+
56
+ # Set or get a component name. Component name is used for finding
57
+ # components within a {Context}.
58
+ #
59
+ # @param [Symbol] name
60
+ # @return [nil] if name is set
61
+ # @return [Symbol] if name is nil
62
+ def component(name = nil)
63
+ if name
64
+ @component = name
65
+
66
+ else
67
+ @component
68
+ end
69
+ end
70
+
71
+ # Pass component name to the subclass.
72
+ def inherited(subclass)
73
+ subclass.component(@component)
74
+ end
75
+ end
76
+
77
+ attr_accessor :context, :atime, :mtime, :ctime
78
+
79
+ # @param [Boolean] bound
80
+ def initialize(bound: false)
81
+ @bound = bound
82
+ @atime = @mtime = @ctime = Time.now
83
+ end
84
+
85
+ # Called by {Factory} when the instance is prepared. Subclasses must call
86
+ # this method.
87
+ def setup
88
+ @children = Children.new(context)
89
+ end
90
+
91
+ # Attempt to find a child component with `name`.
92
+ #
93
+ # @return [HaveAPI::Fs::Component] if found
94
+ # @return [nil] if not found
95
+ def find(name)
96
+ return @children[name] if @children.has_key?(name)
97
+ c = new_child(name)
98
+
99
+ @children.set(name, Factory.create(context, name, *c)) if c
100
+ end
101
+
102
+ # Attempt to find and use nested components with `names`. Each name is for
103
+ # the next descendant. If the target component is found, it and all
104
+ # components in its path will be bound. Bound components are not
105
+ # automatically deleted when not in use.
106
+ def use(*names)
107
+ ret = self
108
+ path = []
109
+
110
+ names.each do |n|
111
+ ret = ret.find(n)
112
+ return if ret.nil?
113
+ path << ret
114
+ end
115
+
116
+ path.each { |c| c.bound = true }
117
+
118
+ ret
119
+ end
120
+
121
+ def bound?
122
+ @bound
123
+ end
124
+
125
+ def bound=(b)
126
+ @bound = b
127
+ end
128
+
129
+ def directory?
130
+ !file?
131
+ end
132
+
133
+ def file?
134
+ !directory?
135
+ end
136
+
137
+ def readable?
138
+ true
139
+ end
140
+
141
+ def writable?
142
+ false
143
+ end
144
+
145
+ def executable?
146
+ false
147
+ end
148
+
149
+ def contents
150
+ raise NotImplementedError
151
+ end
152
+
153
+ def times
154
+ [@atime, @mtime, @ctime]
155
+ end
156
+
157
+ # Shortcut for {#drop_children} and {#setup}.
158
+ def reset
159
+ drop_children
160
+ setup
161
+ end
162
+
163
+ def title
164
+ self.class.name
165
+ end
166
+
167
+ # @return [String] path of this component in the tree without the leading /
168
+ def path
169
+ context.file_path.join('/')
170
+ end
171
+
172
+ # @return [String] absolute path of this component from the system root
173
+ def abspath
174
+ File.join(
175
+ context.mountpoint,
176
+ path
177
+ )
178
+ end
179
+
180
+ def parent
181
+ context.object_path[-2][1]
182
+ end
183
+
184
+ # A component is unsaved if it or any of its descendants has been modified
185
+ # and not saved.
186
+ #
187
+ # @param [Integer] n used to determine the result just once per the same `n`
188
+ # @return [Boolean]
189
+ def unsaved?(n = nil)
190
+ return @is_unsaved if n && @last_unsaved == n
191
+
192
+ child = @children.detect { |_, c| c.unsaved? }
193
+
194
+ @last_unsaved = n
195
+ @is_unsaved = !child.nil?
196
+ end
197
+
198
+ # Mark the component and all its descendats as invalid. Invalid components
199
+ # can still be in the cache and are dropped on hit.
200
+ def invalidate
201
+ @invalid = true
202
+
203
+ children.each { |_, c| c.invalidate }
204
+ end
205
+
206
+ def invalid?
207
+ @invalid
208
+ end
209
+
210
+ protected
211
+ attr_accessor :children
212
+
213
+ # Called to create a component for a child with `name` if this child is not
214
+ # yet or not anymore in memory. All subclasses should extend this method to
215
+ # add their own custom contents.
216
+ #
217
+ # @param [Symbol] name
218
+ # @return [Array] the array describes the new child to be created by
219
+ # {Factory}. The first item is a class name and
220
+ # the rest are arguments to its constructor.
221
+ # @return [nil] if the child does not exist
222
+ def new_child(name)
223
+ raise NotImplementedError
224
+ end
225
+
226
+ # Drop all children from the memory and clear them from the cache.
227
+ def drop_children
228
+ @children.clear
229
+ context.cache.drop_below(path)
230
+ end
231
+
232
+ # Update the time of last modification.
233
+ def changed
234
+ self.mtime = Time.now
235
+ end
236
+ end
237
+ end
@@ -0,0 +1,106 @@
1
+ module HaveAPI::Fs::Components
2
+ class ActionDir < Directory
3
+ component :action_dir
4
+ attr_reader :resource, :action
5
+ children_reader :status, :input, :output
6
+
7
+ def initialize(resource, action)
8
+ super()
9
+
10
+ @resource = resource
11
+ @action = action
12
+ end
13
+
14
+ def setup
15
+ super
16
+
17
+ children[:status] = [ActionStatus, self, bound: true]
18
+ children[:message] = [ActionMessage, self, bound: true]
19
+ children[:errors] = [ActionErrors, self, bound: true]
20
+ children[:input] = [ActionInput, self, bound: true]
21
+ children[:output] = [ActionOutput, self, bound: true]
22
+ children[:exec] = [ActionExec, self, bound: true]
23
+ children[:reset] = [DirectoryReset, bound: true]
24
+ end
25
+
26
+ def contents
27
+ ret = super + %w(input output status message errors exec reset)
28
+ ret << 'exec.yml' if @action.input_params.any?
29
+ ret
30
+ end
31
+
32
+ def exec(meta: {})
33
+ @action.provide_args(*@resource.prepared_args)
34
+ ret = HaveAPI::Client::Response.new(
35
+ @action,
36
+ @action.execute(
37
+ children[:input].values.update({meta: meta})
38
+ )
39
+ )
40
+
41
+ children[:status].set(ret.ok?)
42
+
43
+ if ret.ok?
44
+ case @action.output_layout
45
+ when :object
46
+ res = HaveAPI::Client::ResourceInstance.new(
47
+ @resource.instance_variable_get('@client'),
48
+ @resource.instance_variable_get('@api'),
49
+ @resource,
50
+ action: @action,
51
+ response: ret,
52
+ )
53
+
54
+ when :object_list
55
+ res = HaveAPI::Client::ResourceInstanceList.new(
56
+ @resource.instance_variable_get('@client'),
57
+ @resource.instance_variable_get('@api'),
58
+ @resource,
59
+ @action,
60
+ ret,
61
+ )
62
+
63
+ else
64
+ res = ret
65
+ end
66
+
67
+ children[:output].data = res
68
+
69
+ else
70
+ children[:message].set(ret.message)
71
+ children[:errors].set(ret.errors)
72
+ end
73
+
74
+ ret
75
+
76
+ rescue HaveAPI::Client::ValidationError => e
77
+ children[:status].set(false)
78
+ children[:message].set(e.message)
79
+ children[:errors].set(e.errors)
80
+ e
81
+
82
+ rescue HaveAPI::Client::ActionFailed => e
83
+ children[:status].set(false)
84
+ children[:message].set(e.response.message)
85
+ children[:errors].set(e.response.errors)
86
+ e.response
87
+ end
88
+
89
+ def title
90
+ @action.name.capitalize
91
+ end
92
+
93
+ protected
94
+ def new_child(name)
95
+ if child = super
96
+ child
97
+
98
+ elsif name == :'exec.yml' && @action.input_params.any?
99
+ [ActionExecEdit, self]
100
+
101
+ else
102
+ nil
103
+ end
104
+ end
105
+ end
106
+ end
@@ -0,0 +1,49 @@
1
+ module HaveAPI::Fs::Components
2
+ class ActionErrors < Directory
3
+ component :action_errors
4
+
5
+ class ActionError < File
6
+ def initialize(errors)
7
+ @errors = errors
8
+ end
9
+
10
+ def read
11
+ @errors.join("\n") + "\n"
12
+ end
13
+ end
14
+
15
+ def initialize(action_dir, *args)
16
+ super(*args)
17
+ @action_dir = action_dir
18
+ end
19
+
20
+ def contents
21
+ ret = super
22
+ return ret unless @errors
23
+ ret.concat(@errors.keys.map(&:to_s))
24
+ ret
25
+ end
26
+
27
+ def set(errors)
28
+ changed
29
+ @errors = errors
30
+ end
31
+
32
+ def title
33
+ 'Errors'
34
+ end
35
+
36
+ protected
37
+ def new_child(name)
38
+ if child = super
39
+ child
40
+
41
+ elsif @errors && @errors.has_key?(name)
42
+ [ActionError, @errors[name]]
43
+
44
+ else
45
+ nil
46
+ end
47
+ end
48
+ end
49
+ end
@@ -0,0 +1,12 @@
1
+ module HaveAPI::Fs::Components
2
+ class ActionExec < Executable
3
+ def initialize(action_dir, *args)
4
+ super(*args)
5
+ @action_dir = action_dir
6
+ end
7
+
8
+ def exec
9
+ @action_dir.exec
10
+ end
11
+ end
12
+ end
@@ -0,0 +1,90 @@
1
+ require 'yaml'
2
+
3
+ module HaveAPI::Fs::Components
4
+ class ActionExecEdit < File
5
+ def initialize(action_dir)
6
+ super()
7
+ @action_dir = action_dir
8
+ end
9
+
10
+ def writable?
11
+ true
12
+ end
13
+
14
+ def read
15
+ ret = header + "\n"
16
+
17
+ @action_dir.action.input_params.each do |name, p|
18
+ param_file = @action_dir.find(:input).find(name)
19
+
20
+ if param_file.set?
21
+ v = param_file.new_value
22
+
23
+ elsif p[:default].nil?
24
+ v = nil
25
+
26
+ else
27
+ v = p[:default]
28
+ end
29
+
30
+ ret += "# #{p[:label]}; #{p[:type]}\n"
31
+ ret += "# #{p[:description]}\n"
32
+ ret += "# Defaults to '#{p[:default]}'\n" unless p[:default].nil?
33
+
34
+ if p[:required] || param_file.set?
35
+ ret += "#{name}: #{v}"
36
+
37
+ else
38
+ ret += "##{name}: #{v}"
39
+ end
40
+
41
+ ret += "\n\n"
42
+ end
43
+
44
+ ret
45
+ end
46
+
47
+ def write(str)
48
+ return if str.strip.empty?
49
+
50
+ data = YAML.load(str)
51
+ raise Errno::EIO, 'invalid yaml document' unless data.is_a?(::Hash)
52
+ return unless save?(data)
53
+
54
+ params = @action_dir.action.input_params
55
+
56
+ data.each do |k, v|
57
+ p = @action_dir.find(:input).find(k.to_sym)
58
+ next if p.nil?
59
+
60
+ # Type coercion is done later by the client during action call
61
+ p.write_safe(v)
62
+ end
63
+
64
+ save
65
+ end
66
+
67
+ def header
68
+ <<END
69
+ # This file is in YAML format. Lines beginning with a hash (#) are comments and
70
+ # are ignored. The action will be executed once this file is saved and closed.
71
+ # The success of this operation can be later checked in
72
+ # actions/#{@action_dir.action.name}/status.
73
+ #
74
+ # Only required parameters that need to be set are uncommented by default.
75
+ # Parameters that are not specified when this file is closed will not be sent
76
+ # to the API.
77
+ #
78
+ # To cancel the operation, either do not save the file or save it empty.
79
+ END
80
+ end
81
+
82
+ def save?(data)
83
+ true
84
+ end
85
+
86
+ def save
87
+ @action_dir.exec
88
+ end
89
+ end
90
+ end
@@ -0,0 +1,40 @@
1
+ module HaveAPI::Fs::Components
2
+ class ActionInput < Directory
3
+ component :action_input
4
+ attr_reader :action_dir
5
+
6
+ def initialize(action_dir, *args)
7
+ super(*args)
8
+ @action_dir = action_dir
9
+ end
10
+
11
+ def contents
12
+ super + parameters.keys.map(&:to_s)
13
+ end
14
+
15
+ def parameters
16
+ @action_dir.action.input_params
17
+ end
18
+
19
+ def values
20
+ Hash[children.select { |n, c| c.is_a?(Parameter) && c.set? }.map { |n, c| [n, c.value] }]
21
+ end
22
+
23
+ def title
24
+ 'Input parameters'
25
+ end
26
+
27
+ protected
28
+ def new_child(name)
29
+ if child = super
30
+ child
31
+
32
+ elsif @action_dir.action.input_params.has_key?(name)
33
+ [Parameter, @action_dir.action, name, :input]
34
+
35
+ else
36
+ nil
37
+ end
38
+ end
39
+ end
40
+ end
@@ -0,0 +1,19 @@
1
+ module HaveAPI::Fs::Components
2
+ class ActionMessage < File
3
+ def initialize(action_dir, *args)
4
+ super(*args)
5
+ @action_dir = action_dir
6
+ end
7
+
8
+ def read
9
+ ret = @msg.to_s
10
+ ret += "\n" unless ret.empty?
11
+ ret
12
+ end
13
+
14
+ def set(msg)
15
+ changed
16
+ @msg = msg
17
+ end
18
+ end
19
+ end
@@ -0,0 +1,79 @@
1
+ module HaveAPI::Fs::Components
2
+ class ActionOutput < Directory
3
+ component :action_output
4
+ attr_reader :action_dir
5
+ attr_accessor :data
6
+
7
+ def initialize(action_dir, *args)
8
+ super(*args)
9
+
10
+ @action_dir = action_dir
11
+
12
+ if %i(hash_list object_list).include?(@action_dir.action.output_layout.to_sym)
13
+ @list = true
14
+ end
15
+ end
16
+
17
+ def contents
18
+ ret = super
19
+
20
+ return ret unless @data
21
+
22
+ if @list
23
+ if @data.is_a?(HaveAPI::Client::ResourceInstanceList)
24
+ ret.concat(@data.map { |v| v.id.to_s })
25
+
26
+ else
27
+ ret.concat(@data.response.map { |v| v[:id].to_s })
28
+ end
29
+
30
+ else
31
+ ret.concat(parameters.keys.map(&:to_s))
32
+ end
33
+
34
+ ret
35
+ end
36
+
37
+ def parameters
38
+ @action_dir.action.params
39
+ end
40
+
41
+ def title
42
+ 'Output parameters'
43
+ end
44
+
45
+ protected
46
+ def new_child(name)
47
+ if child = super
48
+ child
49
+
50
+ elsif !@data
51
+ nil
52
+
53
+ elsif @list
54
+ id = name.to_s.to_i
55
+
56
+ if @data.is_a?(HaveAPI::Client::ResourceInstanceList)
57
+ param = @data.detect { |v| v.id == id }
58
+ [ResourceInstanceDir, param]
59
+
60
+ else
61
+ param = @data.response.detect { |v| v[:id] == id }
62
+ [ListItem, @action_dir.action, :output, param]
63
+ end
64
+
65
+ elsif @action_dir.action.params.has_key?(name)
66
+ [
67
+ Parameter,
68
+ @action_dir.action,
69
+ name,
70
+ :output,
71
+ @data.is_a?(HaveAPI::Client::ResourceInstance) ? @data : @data.response,
72
+ ]
73
+
74
+ else
75
+ nil
76
+ end
77
+ end
78
+ end
79
+ end
@@ -0,0 +1,32 @@
1
+ module HaveAPI::Fs::Components
2
+ class ActionStatus < File
3
+ def initialize(action_dir, *args)
4
+ super(*args)
5
+ @action_dir = action_dir
6
+ @v = nil
7
+ end
8
+
9
+ def read
10
+ value.to_s + "\n"
11
+ end
12
+
13
+ def set(v)
14
+ changed
15
+ @v = v
16
+ end
17
+
18
+ protected
19
+ def value
20
+ case @v
21
+ when true
22
+ 1
23
+
24
+ when false
25
+ 0
26
+
27
+ else
28
+ nil
29
+ end
30
+ end
31
+ end
32
+ end
@@ -0,0 +1,19 @@
1
+ module HaveAPI::Fs::Components
2
+ class CacheStats < File
3
+ def read
4
+ c = context.cache
5
+
6
+ {
7
+ size: c.size,
8
+ hits: c.hits,
9
+ misses: c.misses,
10
+ invalid: c.invalid,
11
+ drops: c.drops,
12
+ hitratio: (c.hits.to_f / (c.hits + c.misses + c.invalid) * 100).round(2),
13
+ sweeps: c.runs,
14
+ last_sweep: (c.last_time && c.last_time.iso8601) || '-',
15
+ next_sweep: c.next_time.iso8601,
16
+ }.map { |k, v| sprintf('%-15s %s', "#{k}:", v) }.join("\n") + "\n"
17
+ end
18
+ end
19
+ end
@@ -0,0 +1,24 @@
1
+ module HaveAPI::Fs::Components
2
+ class ComponentList < File
3
+ def read
4
+ str = component_list.map do |c|
5
+ sprintf('%-50s %s', c.class.name, c.path)
6
+ end.join("\n")
7
+ str += "\n" unless str.empty?
8
+ str
9
+ end
10
+
11
+ protected
12
+ def component_list(component = nil)
13
+ component ||= parent
14
+ ret = []
15
+
16
+ component.send(:children).each do |_, c|
17
+ ret << c
18
+ ret.concat(component_list(c))
19
+ end
20
+
21
+ ret
22
+ end
23
+ end
24
+ end