impromptu 1.0.0

Sign up to get free protection for your applications and to get access to all the features.
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