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.
- 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
|