haveapi-fs 0.1.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.
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