impromptu 1.0.0
Sign up to get free protection for your applications and to get access to all the features.
- data/.document +5 -0
- data/.gitignore +21 -0
- data/LICENSE +34 -0
- data/README.rdoc +76 -0
- data/Rakefile +53 -0
- data/VERSION +1 -0
- data/impromptu.gemspec +106 -0
- data/lib/impromptu/autoload.rb +33 -0
- data/lib/impromptu/component.rb +124 -0
- data/lib/impromptu/component_set.rb +10 -0
- data/lib/impromptu/file.rb +220 -0
- data/lib/impromptu/folder.rb +151 -0
- data/lib/impromptu/impromptu.rb +81 -0
- data/lib/impromptu/ordered_set.rb +49 -0
- data/lib/impromptu/resource.rb +195 -0
- data/lib/impromptu/symbol.rb +43 -0
- data/lib/impromptu.rb +12 -0
- data/test/framework/copies/extra_klass2.rb +7 -0
- data/test/framework/copies/new_klass.rb +10 -0
- data/test/framework/copies/new_unseen.rb +7 -0
- data/test/framework/copies/original_klass.rb +10 -0
- data/test/framework/ext/extensions/blog.rb +6 -0
- data/test/framework/ext/extensions.rb +4 -0
- data/test/framework/lib/group/klass2.rb +4 -0
- data/test/framework/lib/klass.rb +10 -0
- data/test/framework/other/also.rb +8 -0
- data/test/framework/other/ignore.rb +2 -0
- data/test/framework/other/load.rb +2 -0
- data/test/framework/other/two.rb +14 -0
- data/test/framework/private/klass.rb +10 -0
- data/test/framework/test.components +23 -0
- data/test/helper.rb +10 -0
- data/test/test_autoload.rb +32 -0
- data/test/test_component.rb +133 -0
- data/test/test_component_set.rb +20 -0
- data/test/test_folder.rb +4 -0
- data/test/test_impromptu.rb +43 -0
- data/test/test_integration.rb +312 -0
- data/test/test_ordered_set.rb +93 -0
- data/test/test_resource.rb +186 -0
- data/test/test_symbol.rb +99 -0
- 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
|