impromptu 1.0.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 (42) hide show
  1. data/.document +5 -0
  2. data/.gitignore +21 -0
  3. data/LICENSE +34 -0
  4. data/README.rdoc +76 -0
  5. data/Rakefile +53 -0
  6. data/VERSION +1 -0
  7. data/impromptu.gemspec +106 -0
  8. data/lib/impromptu/autoload.rb +33 -0
  9. data/lib/impromptu/component.rb +124 -0
  10. data/lib/impromptu/component_set.rb +10 -0
  11. data/lib/impromptu/file.rb +220 -0
  12. data/lib/impromptu/folder.rb +151 -0
  13. data/lib/impromptu/impromptu.rb +81 -0
  14. data/lib/impromptu/ordered_set.rb +49 -0
  15. data/lib/impromptu/resource.rb +195 -0
  16. data/lib/impromptu/symbol.rb +43 -0
  17. data/lib/impromptu.rb +12 -0
  18. data/test/framework/copies/extra_klass2.rb +7 -0
  19. data/test/framework/copies/new_klass.rb +10 -0
  20. data/test/framework/copies/new_unseen.rb +7 -0
  21. data/test/framework/copies/original_klass.rb +10 -0
  22. data/test/framework/ext/extensions/blog.rb +6 -0
  23. data/test/framework/ext/extensions.rb +4 -0
  24. data/test/framework/lib/group/klass2.rb +4 -0
  25. data/test/framework/lib/klass.rb +10 -0
  26. data/test/framework/other/also.rb +8 -0
  27. data/test/framework/other/ignore.rb +2 -0
  28. data/test/framework/other/load.rb +2 -0
  29. data/test/framework/other/two.rb +14 -0
  30. data/test/framework/private/klass.rb +10 -0
  31. data/test/framework/test.components +23 -0
  32. data/test/helper.rb +10 -0
  33. data/test/test_autoload.rb +32 -0
  34. data/test/test_component.rb +133 -0
  35. data/test/test_component_set.rb +20 -0
  36. data/test/test_folder.rb +4 -0
  37. data/test/test_impromptu.rb +43 -0
  38. data/test/test_integration.rb +312 -0
  39. data/test/test_ordered_set.rb +93 -0
  40. data/test/test_resource.rb +186 -0
  41. data/test/test_symbol.rb +99 -0
  42. metadata +139 -0
@@ -0,0 +1,220 @@
1
+ module Impromptu
2
+ class File
3
+ attr_reader :path, :folder, :resources, :related_resources, :related_files, :modified_time
4
+ RELOADABLE_EXTENSIONS = %w{rb}
5
+
6
+ def initialize(path, folder, provides=[])
7
+ @path = path
8
+ @folder = folder
9
+ @resources = OrderedSet.new
10
+ @related_resources = Set.new
11
+ @related_files = Set.new
12
+ @frozen = false
13
+ @modified_time = nil
14
+ @dirty = true
15
+ @provides = [provides].compact.flatten
16
+ end
17
+
18
+ # Override eql? so two files with the same path will be
19
+ # considered equal by ordered set.
20
+ def eql?(other)
21
+ other.path == @path
22
+ end
23
+
24
+ # Override hash so two files with the same path will result
25
+ # in the same hash value.
26
+ def hash
27
+ @path.hash
28
+ end
29
+
30
+ # Traverse the file/resource graph to determine which total
31
+ # set of files and resources are related to this file. For
32
+ # instance, if a resource provided by this file is also
33
+ # defined in another, both files, and the total set of
34
+ # resources provided by these files, are included. If
35
+ # further resources are provided, those resources along
36
+ # with any other files defining them, are also included.
37
+ def freeze
38
+ return if @frozen
39
+ remaining_resources = @resources.to_a
40
+ remaining_files = [self]
41
+
42
+ while remaining_resources.size > 0 || remaining_files.size > 0
43
+ if resource = remaining_resources.shift
44
+ @related_resources << resource
45
+ resource.files.each do |file|
46
+ remaining_files << file unless @related_files.include?(file)
47
+ end
48
+ end
49
+
50
+ if file = remaining_files.shift
51
+ @related_files << file
52
+ file.add_related_file(self)
53
+ file.resources.each do |resource|
54
+ remaining_resources << resource unless @related_resources.include?(resource)
55
+ end
56
+ end
57
+ end
58
+
59
+ @frozen = true
60
+ end
61
+
62
+ # Unfreeze this file by clearing the related files and
63
+ # resources lists.
64
+ def unfreeze
65
+ @related_resources = Set.new
66
+ @related_files = Set.new
67
+ @frozen = false
68
+ end
69
+
70
+ # Unfreeze and freeze a files association lists
71
+ def refreeze
72
+ unfreeze
73
+ freeze
74
+ end
75
+
76
+ # Load (or reload) all of the resources provided by this
77
+ # file. If a resource is provided by multiple files, those
78
+ # files will also be reloaded (and the resources provided
79
+ # in those files reloaded). For this reason it's advisable
80
+ # to declare a single resource per file, otherwise the
81
+ # dependencies between files and resources can cause large
82
+ # subsections of the component graph to be reloaded together.
83
+ def reload
84
+ @related_resources.each {|resource| resource.unload}
85
+ @related_files.each {|file| file.load}
86
+ end
87
+
88
+ # Load the file is possible, after loading any external
89
+ # dependencies for the component owning this file. The
90
+ # modified time is updated so we know if a change is made
91
+ # to the file at a later date. This does a simple load of
92
+ # the file and doesn't unload any resources beforehand.
93
+ # You typically want to use reload to ensure that the
94
+ # resources provided by this file are fully unloaded and
95
+ # reloaded in one go.
96
+ def load
97
+ @folder.component.load_external_dependencies
98
+ Kernel.load @path if reloadable?
99
+ @modified_time = @path.mtime
100
+ @dirty = false
101
+ end
102
+
103
+ # Unload all of the resources provided by this file. This
104
+ # doesn't just unload the parts of a resource defined by
105
+ # this file, but the entire resource itself. i.e if there
106
+ # are two files defining a resource, unloading one file
107
+ # will unload the resource completely.
108
+ def unload
109
+ resources.each {|resource| resource.unload}
110
+ @modified_time = nil
111
+ @dirty = true
112
+ end
113
+
114
+ # Returns true if the current modification time of the
115
+ # underlying file is greater than the modified_time of
116
+ # the file when we last loaded it.
117
+ def modified?
118
+ resources_loaded? && (@dirty || @modified_time.nil? || (@path.mtime > @modified_time))
119
+ end
120
+
121
+ # Reloads the associated resources only if the underlying
122
+ # file has been modified since the last time it was loaded.
123
+ def reload_if_modified
124
+ reload if modified?
125
+ end
126
+
127
+ # Indicates if this file is currently loaded
128
+ def loaded?
129
+ !@modified_time.nil?
130
+ end
131
+
132
+ # True if any of the resources provided by this file have
133
+ # been loaded.
134
+ def resources_loaded?
135
+ @resources.each do |resource|
136
+ return true if resource.loaded?
137
+ end
138
+ false
139
+ end
140
+
141
+ # Add a file to the list of files related to this file.
142
+ def add_related_file(file)
143
+ @related_files << file
144
+ @dirty = true
145
+ end
146
+
147
+ # Remove a file from the list of files related to this file.
148
+ def remove_related_file(file)
149
+ @related_files.delete(file)
150
+ @dirty = true
151
+ end
152
+
153
+ # Delete references to this file from any resources or other
154
+ # files. This does not unload the resource, so if loaded, the
155
+ # resource will be defined by a file which is no longer tracked.
156
+ def remove
157
+ @related_files.each {|file| file.remove_related_file(self)}
158
+ @resources.each do |resource|
159
+ if resource.files.size == 1
160
+ resource.remove
161
+ else
162
+ resource.remove_file(self)
163
+ end
164
+ end
165
+ end
166
+
167
+ # Should only be called once, and only after a file has been
168
+ # loaded for the first time. If the file was defined with
169
+ # a list of resources the file implements, inform each resource
170
+ # that this file implements the resource. Otherwise the name
171
+ # of the file is used to determine the resource provided (e.g
172
+ # klass_one.rb would be assumed to provide KlassOne).
173
+ def add_resource_definition
174
+ # unless defined, we assume the name of the file implies
175
+ # the resource that is provided by the file
176
+ if @provides.empty?
177
+ @provides << module_symbol_from_path
178
+ end
179
+
180
+ # add a reference of this file to every appropriate resource
181
+ @provides.each do |resource_name|
182
+ resource = Impromptu.root_resource.get_or_create_child(combine_symbol_with_namespace(resource_name))
183
+ resource.add_file(self)
184
+ @resources << resource
185
+ end
186
+ end
187
+
188
+ # True if the file has never been loaded before, or if the
189
+ # file is of a type that can be reloaded (i.e ruby source
190
+ # as opposed to C extensions).
191
+ def reloadable?
192
+ return true if !loaded?
193
+ RELOADABLE_EXTENSIONS.include?(@path.extname[1..-1])
194
+ end
195
+
196
+
197
+ private
198
+ # Turn the path of this file into a module name. e.g:
199
+ # /root/plugin/klass_one.rb => Plugin::KlassOne
200
+ def module_symbol_from_path
201
+ # remove any directory names from the path, and the file extension
202
+ extension = @path.extname
203
+ name = @folder.relative_path_to(@path).to_s[0..-(extension.length + 1)]
204
+
205
+ # convert any names following a slash to namespaces
206
+ name.gsub!(/\/(.?)/) {|character| "::#{character[1].upcase}" }
207
+
208
+ # upcase the first character, and any characters following an underscore
209
+ name.gsub(/(?:^|_)(.)/) {|character| character.upcase}.to_sym
210
+ end
211
+
212
+ def combine_symbol_with_namespace(symbol)
213
+ if @folder.component.namespace
214
+ "#{@folder.component.namespace}::#{symbol}".to_sym
215
+ else
216
+ symbol.to_sym
217
+ end
218
+ end
219
+ end
220
+ end
@@ -0,0 +1,151 @@
1
+ module Impromptu
2
+ class Folder
3
+ attr_accessor :folder, :files, :component, :namespace
4
+ DEFAULT_OPTIONS = {nested_namespaces: true, reloadable: true}
5
+ SOURCE_EXTENSIONS = %w{rb so bundle}
6
+
7
+ # Register a new folder containing source files for a
8
+ # component. Path is a Pathname object representing the
9
+ # folder, and options may be:
10
+ # * nested_namespaces: true by default. If true, sub-
11
+ # folders indicate a new namespace for resources. For
12
+ # instance, a folder with a root of 'src', containing
13
+ # a folder called 'plugins' which has a file 'klass.rb'
14
+ # would define the resource Plugins::Klass. When false,
15
+ # the file would simply represent Klass.
16
+ # * reloadable: true by default. If true, this folder
17
+ # will be reloaded every time Impromptu.update is
18
+ # called, and any modified files will be reloaded,
19
+ # removed files unloaded, and new files tracked.
20
+ def initialize(path, component, options={}, block)
21
+ @folder = path.realpath
22
+ @component = component
23
+ @options = DEFAULT_OPTIONS.merge(options)
24
+ @block = block
25
+ @files = OrderedSet.new
26
+ @implicitly_load_all_files = true
27
+ end
28
+
29
+ # Override eql? so two folders with the same path will be
30
+ # considered equal by ordered set.
31
+ def eql?(other)
32
+ other.folder == @folder
33
+ end
34
+
35
+ # Override hash so two folders with the same path will result
36
+ # in the same hash value.
37
+ def hash
38
+ @folder.hash
39
+ end
40
+
41
+ # Load a folders definition and files after the set of components
42
+ # has been defined. For folders provided a block, the block is
43
+ # run in the context of this folder (allowing 'file' to be called).
44
+ # Otherwise the folder is scanned to produce an initial set of files
45
+ # and resources provided by this folder.
46
+ def load
47
+ if @block.nil?
48
+ self.reload_file_set
49
+ else
50
+ instance_eval &@block
51
+ @block = nil # prevent the block from being run twice
52
+ end
53
+ end
54
+
55
+ # True if the folder uses nested namespaces
56
+ def nested_namespaces?
57
+ @options[:nested_namespaces]
58
+ end
59
+
60
+ # True if the folder is reloadable
61
+ def reloadable?
62
+ @options[:reloadable]
63
+ end
64
+
65
+ # Return the 'base' folder for a file contained within this
66
+ # folder. For instance, a folder with nested namespaces would
67
+ # return the path to a file from the root folder. Without
68
+ # namespaces, the relative path to a file would be the enclosing
69
+ # folder of the file. i.e relative path to framework/a/b.rb (where
70
+ # framework is the root folder) would return 'a/b.rb' for a
71
+ # nested namespace folder, or just 'b.rb' for a non nested
72
+ # namespace folder.
73
+ def relative_path_to(path)
74
+ if nested_namespaces?
75
+ path.relative_path_from(@folder)
76
+ else
77
+ path.basename
78
+ end
79
+ end
80
+
81
+ # Explicitly include a file from this folder. If you use this
82
+ # method, only files included by this method will be loaded.
83
+ # If you do not use this method, all files within this folder
84
+ # will be accessible. For this reason, you probably want to
85
+ # separate out files which need to be defined this way into a
86
+ # folder separate to the majority of your source files. Options
87
+ # may be:
88
+ # * provides (required): an array of symbols, or a symbol
89
+ # inidicating the name of the resource(s) provided by the file
90
+ def file(name, options={})
91
+ @implicitly_load_all_files = false
92
+ file = Impromptu::File.new(@folder.join(*name).realpath, self, options[:provides])
93
+ @files.push(file).add_resource_definition
94
+ end
95
+
96
+ # Reload the files provided by this folder. If the folder
97
+ # is tracking a specific set of files, only those files
98
+ # will be reloaded. Otherwise, the folder is scanned again
99
+ # and any previously unseen files loaded, existing files
100
+ # reloaded, and removed files unloaded.
101
+ def reload
102
+ reload_file_set if @implicitly_load_all_files
103
+ @files.each {|file| file.reload_if_modified}
104
+ end
105
+
106
+ # Determine changes between the set of files this folder
107
+ # knows about, and the set of files existing on disk. Any
108
+ # files which have been removed are unloaded (and their
109
+ # resources reloaded if other files define the resources
110
+ # as well), and any new files insert their resources in to
111
+ # the known resources tree.
112
+ def reload_file_set
113
+ return unless @implicitly_load_all_files
114
+ old_file_set = @files.to_a
115
+ new_file_set = []
116
+ changes = false
117
+
118
+ # find all current files and add them if necessary
119
+ @folder.find do |path|
120
+ next unless source_file?(path)
121
+ file = Impromptu::File.new(path.realpath, self)
122
+ new_file_set << file
123
+ unless @files.include?(file)
124
+ changes = true
125
+ @files.push(file).add_resource_definition
126
+ end
127
+ end
128
+
129
+ # remove any files which have been deleted
130
+ deleted_files = old_file_set - new_file_set
131
+ deleted_files.each {|file| @files.delete(file).remove}
132
+ changes = true if deleted_files.size > 0
133
+
134
+ # refreeze each files association lists if the set of
135
+ # files has changed
136
+ if changes
137
+ @files.each do |file|
138
+ file.refreeze
139
+ end
140
+ end
141
+ end
142
+
143
+
144
+ private
145
+ # True if the path represents a file with an extension known to
146
+ # implement resources.
147
+ def source_file?(path)
148
+ path.file? && SOURCE_EXTENSIONS.include?(path.extname[1..-1])
149
+ end
150
+ end
151
+ end
@@ -0,0 +1,81 @@
1
+ module Impromptu
2
+ # Resources are represented in a tree, with the root_resource
3
+ # representing the Object object. All resources are children
4
+ # of this resource.
5
+ def self.root_resource
6
+ @root_resource ||= Resource.new(:Object, nil)
7
+ end
8
+
9
+ # The set of components known to Impromptu
10
+ def self.components
11
+ @components ||= ComponentSet.new
12
+ end
13
+
14
+ # Call update to reload any folders (and associated files) which
15
+ # have been marked as reloadable. Any modified files will be
16
+ # reloaded, any new files will have their assiciated resources
17
+ # inserted in the resource tree, and any removed files will be
18
+ # unloaded.
19
+ def self.update
20
+ components.each do |component|
21
+ component.folders.each do |folder|
22
+ folder.reload if folder.reloadable?
23
+ end
24
+ end
25
+ end
26
+
27
+ # Reset Impromptu by removing all known components and resources.
28
+ # This should rarely be used in a running application and mainly
29
+ # exists to allow the test framework to run the same setup code
30
+ # multiple times without changing stale components.
31
+ def self.reset
32
+ # unload all resources
33
+ unless @root_resource.nil?
34
+ @root_resource.children.each_value do |resource|
35
+ resource.unload
36
+ end
37
+ end
38
+
39
+ # reset lists to nil
40
+ @root_resource = nil
41
+ @components = nil
42
+ @base = nil
43
+ end
44
+
45
+ # Parse component definition files, or create components as
46
+ # necessary directly from the supplied block. The block is run
47
+ # in the context of the Impromptu module allowing you to call
48
+ # 'component' directly without reference to Impromptu.
49
+ def self.define_components(base=nil, &block)
50
+ # load the component definitions
51
+ raise "No block supplied to define_components" unless block_given?
52
+ @base = base || Pathname.new('.').realpath
53
+ instance_eval &block
54
+
55
+ # now that we have a complete file/resource graph, freeze
56
+ # the associations at this point (will be unfrozen for reloads)
57
+ components.each do |component|
58
+ component.freeze
59
+ end
60
+ end
61
+
62
+ # Open and run a file defining components. The folder containing
63
+ # the file is used as the 'base' folder, and folder references
64
+ # within components are assumed to be relative to this base.
65
+ def self.parse_file(path)
66
+ @base = Pathname.new(path).realpath.dirname
67
+ ::File.open(path) do |file|
68
+ instance_eval file.read
69
+ end
70
+ end
71
+
72
+ # Define and create a new component. The name of the component
73
+ # must be unique (otherwise you will get a reference to the
74
+ # existing component), and the block supplied is run in the context
75
+ # of the newly created component.
76
+ def self.component(name, &block)
77
+ component = components << Component.new(@base, name)
78
+ component.instance_eval &block if block_given?
79
+ component.folders.each {|folder| folder.load}
80
+ end
81
+ end
@@ -0,0 +1,49 @@
1
+ module Impromptu
2
+ class OrderedSet
3
+ include Enumerable
4
+
5
+ def initialize
6
+ @items_hash = {}
7
+ @items_list = []
8
+ end
9
+
10
+ def <<(item)
11
+ unless self.include?(item)
12
+ @items_hash[item] = item
13
+ @items_list << item
14
+ end
15
+ @items_hash[item]
16
+ end
17
+ alias :push :<<
18
+
19
+ def merge(items)
20
+ items.each {|item| self.<< item}
21
+ end
22
+
23
+ def delete(item)
24
+ self.include?(item) or return nil
25
+ @items_hash.delete(item)
26
+ @items_list.delete(item)
27
+ end
28
+
29
+ def include?(item)
30
+ @items_hash.has_key?(item)
31
+ end
32
+
33
+ def to_a
34
+ @items_list.dup
35
+ end
36
+
37
+ def each(&block)
38
+ @items_list.each {|item| yield item}
39
+ end
40
+
41
+ def size
42
+ @items_list.size
43
+ end
44
+
45
+ def empty?
46
+ @items_list.empty?
47
+ end
48
+ end
49
+ end
@@ -0,0 +1,195 @@
1
+ module Impromptu
2
+ # Resources represent the modules or classes that are tracked by
3
+ # Impromptu and which can be lazy loaded. You should never create
4
+ # resources yourself - use component definitions to define folders
5
+ # of files which will implement resources.
6
+ class Resource
7
+ attr_reader :name, :base_symbol, :files, :children, :parent
8
+
9
+ def initialize(name, parent)
10
+ @name = name.to_sym
11
+ @parent = parent
12
+ @base_symbol = @name.base_symbol
13
+ @files = OrderedSet.new
14
+ @children = {}
15
+ @reference = nil
16
+ @namespace = false
17
+ @implicitly_defined = true
18
+ end
19
+
20
+ # Override eql? so two resources with the same name will be
21
+ # considered equal by ordered set. Names are fully namespaced
22
+ # so will always be unique.
23
+ def eql?(other)
24
+ other.name == @name
25
+ end
26
+
27
+ # Override hash so two resources with the same name will result
28
+ # in the same hash value. Names are fully namespaced so will
29
+ # always be unique.
30
+ def hash
31
+ @name.hash
32
+ end
33
+
34
+ # Attempts to retrieve a reference to the object represented by
35
+ # this resource. If the resource is unloaded, nil is returned.
36
+ def reference
37
+ return Object if root?
38
+ return nil unless @parent && @parent.reference
39
+ @reference ||= @parent.reference.const_get(@base_symbol)
40
+ end
41
+
42
+ # Reload the implementation of this resource from all files being
43
+ # tracked for this resource. Call this method if you are manually
44
+ # managing file tracking, and need to guarantee all files for this
45
+ # resource have been loaded. It is normally unecessary to call this
46
+ # if you rely on the autoloader and reloader.
47
+ def reload
48
+ @parent.reload unless @parent.loaded?
49
+ if @implicitly_defined
50
+ self.unload
51
+ Object.module_eval "module #{@name}; end"
52
+ else
53
+ @files.first.reload
54
+ end
55
+ end
56
+
57
+ # Unload the resource by undefining the constant representing it.
58
+ # Any resources contained within this resource will also be
59
+ # unloaded. This allows the resource to be garbage collected.
60
+ def unload
61
+ return unless loaded?
62
+ @children.each_value {|child| child.unload}
63
+ @parent.reference.send(:remove_const, @base_symbol)
64
+ @reference = nil
65
+ end
66
+
67
+ # Start tracking a file which implements this resource. If the
68
+ # file has already been added it won't be added a second time.
69
+ # This method does not load the added file, so the definition
70
+ # of the resource will be incomplete until reload is called.
71
+ def add_file(file)
72
+ @files << file
73
+ @implicitly_defined = false
74
+ end
75
+
76
+ # Un-track a file implementing this resource. If the file was
77
+ # never tracked, no error is raised. This does not reload the
78
+ # resource, so the resource will be based on a stale definition
79
+ # if it was previously loaded.
80
+ def remove_file(file)
81
+ @files.delete(file)
82
+ @implicitly_defined = true if @files.size == 0 && namespace?
83
+ end
84
+
85
+ # True if no files have been associated with this resource. In
86
+ # this case, loading the resource will be achieved by creating
87
+ # a blank module with the name of the resource. If any files
88
+ # have been associated, they will be loaded instead, and this
89
+ # method will return false.
90
+ def implicitly_defined?
91
+ @implicitly_defined
92
+ end
93
+
94
+ # True if this resource represents a module acting as a
95
+ # namespace.
96
+ def namespace?
97
+ @namespace
98
+ end
99
+
100
+ # Mark a resource as representing a module acting as a
101
+ # namespace.
102
+ def namespace!
103
+ @namespace = true
104
+ end
105
+
106
+ # True if this resource is the parent of any resources.
107
+ def children?
108
+ !@children.empty?
109
+ end
110
+
111
+ # Add a child resource to this resource. This does not load the
112
+ # resource. This should only be called by get_or_create_child.
113
+ def add_child(resource)
114
+ @children[resource.base_symbol] = resource
115
+ end
116
+
117
+ # Remove a reference to a child resource. This does not unload
118
+ # the object, but is called automatically by unload on its parent
119
+ # resource.
120
+ def remove_child(resource)
121
+ @children.delete(resource.base_symbol)
122
+ end
123
+
124
+ # In most instances, this method should only be called on the
125
+ # root resource to traverse the resource tree and retrieve or
126
+ # create the specified resource. Name must be a symbol.
127
+ def get_or_create_child(name)
128
+ return nil unless name.is_a?(Symbol)
129
+ current = self
130
+
131
+ name.each_namespaced_symbol do |name|
132
+ # attempt to get the current resource
133
+ child_resource = current.child(name.base_symbol)
134
+
135
+ # create the resource if needed
136
+ if child_resource.nil?
137
+ child_resource = Resource.new(name, current)
138
+ current.add_child(child_resource)
139
+ end
140
+
141
+ # the next current resource is the one we just created or retrieved
142
+ current = child_resource
143
+ end
144
+
145
+ # after iterating through the name, current will be the resulting resource
146
+ current
147
+ end
148
+
149
+ # Walks the resource tree and returns the resource corresponding
150
+ # to name (which must be a symbol and can be namespaced). If the
151
+ # resource doesn't exist, nil is returned
152
+ def child(name)
153
+ return nil unless name.is_a?(Symbol)
154
+ return @children[name] if name.unnested?
155
+
156
+ # if the name is nested, walk the resource tree to return the
157
+ # resource under this branch. rerturn nil if we reach a
158
+ # branch which doesn't exist
159
+ nested_symbols = name.nested_symbols
160
+ top_level_symbol = nested_symbols.first
161
+ further_symbols = nested_symbols[1..-1].join('::').to_sym
162
+ return nil unless @children.has_key?(top_level_symbol)
163
+ @children[top_level_symbol].child(further_symbols)
164
+ end
165
+
166
+ # Walks the resource tree to determine if the resource referred
167
+ # to by name (which must be a symbol, and may be namespaced) is
168
+ # known by Impromptu.
169
+ def child?(name)
170
+ !self.child(name).nil?
171
+ end
172
+
173
+ # Unload and remove all references to this resource.
174
+ def remove
175
+ unload
176
+ @parent.remove_child(self) if @parent
177
+ end
178
+
179
+ # True if this resource is the root resource referring to Object.
180
+ def root?
181
+ @parent.nil?
182
+ end
183
+
184
+ # True if this resource exists as a constant in its parent.
185
+ # This does not guarantee that every file implementing this
186
+ # resource has been loaded, however, so an incomplete instance
187
+ # may exist. If you rely on the autoloader and reloader this
188
+ # will not occur.
189
+ def loaded?
190
+ return true if root?
191
+ return false unless @parent && @parent.loaded? && @parent.reference
192
+ parent.reference.constants.include?(@base_symbol)
193
+ end
194
+ end
195
+ end