rake-pipeline 0.5.0 → 0.7.0
Sign up to get free protection for your applications and to get access to all the features.
- data/.travis.yml +12 -0
- data/Gemfile +1 -0
- data/README.markdown +1 -1
- data/README.yard +61 -32
- data/Rakefile +9 -0
- data/bin/rakep +1 -24
- data/lib/generators/rake/pipeline/install/install_generator.rb +70 -0
- data/lib/rake-pipeline.rb +117 -53
- data/lib/rake-pipeline/cli.rb +56 -0
- data/lib/rake-pipeline/dsl.rb +3 -140
- data/lib/rake-pipeline/dsl/pipeline_dsl.rb +168 -0
- data/lib/rake-pipeline/dsl/project_dsl.rb +108 -0
- data/lib/rake-pipeline/dynamic_file_task.rb +188 -0
- data/lib/rake-pipeline/file_wrapper.rb +1 -1
- data/lib/rake-pipeline/filter.rb +45 -15
- data/lib/rake-pipeline/filters.rb +3 -1
- data/lib/rake-pipeline/filters/{concat.rb → concat_filter.rb} +0 -0
- data/lib/rake-pipeline/filters/ordering_concat_filter.rb +38 -0
- data/lib/rake-pipeline/filters/pipeline_finalizing_filter.rb +19 -0
- data/lib/rake-pipeline/graph.rb +178 -0
- data/lib/rake-pipeline/manifest.rb +63 -0
- data/lib/rake-pipeline/manifest_entry.rb +34 -0
- data/lib/rake-pipeline/matcher.rb +65 -30
- data/lib/rake-pipeline/middleware.rb +15 -12
- data/lib/rake-pipeline/precompile.rake +8 -0
- data/lib/rake-pipeline/project.rb +280 -0
- data/lib/rake-pipeline/railtie.rb +16 -1
- data/lib/rake-pipeline/server.rb +15 -0
- data/lib/rake-pipeline/version.rb +2 -2
- data/rake-pipeline.gemspec +2 -0
- data/spec/cli_spec.rb +71 -0
- data/spec/concat_filter_spec.rb +1 -27
- data/spec/{dsl_spec.rb → dsl/pipeline_dsl_spec.rb} +32 -18
- data/spec/dsl/project_dsl_spec.rb +41 -0
- data/spec/dynamic_file_task_spec.rb +111 -0
- data/spec/encoding_spec.rb +6 -8
- data/spec/file_wrapper_spec.rb +19 -2
- data/spec/filter_spec.rb +120 -22
- data/spec/graph_spec.rb +56 -0
- data/spec/manifest_entry_spec.rb +51 -0
- data/spec/manifest_spec.rb +67 -0
- data/spec/matcher_spec.rb +35 -2
- data/spec/middleware_spec.rb +123 -75
- data/spec/ordering_concat_filter_spec.rb +39 -0
- data/spec/pipeline_spec.rb +95 -34
- data/spec/project_spec.rb +293 -0
- data/spec/rake_acceptance_spec.rb +307 -67
- data/spec/rake_tasks_spec.rb +21 -0
- data/spec/spec_helper.rb +11 -48
- data/spec/support/spec_helpers/file_utils.rb +35 -0
- data/spec/support/spec_helpers/filters.rb +16 -0
- data/spec/support/spec_helpers/input_helpers.rb +23 -0
- data/spec/support/spec_helpers/memory_file_wrapper.rb +31 -0
- data/tools/perfs +107 -0
- metadata +100 -12
@@ -0,0 +1,188 @@
|
|
1
|
+
module Rake
|
2
|
+
class Pipeline
|
3
|
+
# This class extends Rake's {Rake::FileTask} class to add support
|
4
|
+
# for dynamic dependencies. Typically, Rake handles static dependencies,
|
5
|
+
# where a file's dependencies are known before the task is invoked.
|
6
|
+
# A {DynamicFileTask} also supports dynamic dependencies, meaning the
|
7
|
+
# file's dependencies can be determined just before invoking the task.
|
8
|
+
# Because calculating a file's dependencies at runtime may be an expensive
|
9
|
+
# operation (it could involve reading the file from disk and parsing it
|
10
|
+
# to extract dependency information, for example), the results of this
|
11
|
+
# calculation are stored on disk in a manifest file, and reused on
|
12
|
+
# subsequent runs if possible.
|
13
|
+
#
|
14
|
+
# For example, consider this file app.c:
|
15
|
+
#
|
16
|
+
# #include "app.h"
|
17
|
+
# some_stuff();
|
18
|
+
#
|
19
|
+
# If we have a task that compiles app.c into app.o, it needs to
|
20
|
+
# process app.c to look for additional dependencies specified
|
21
|
+
# by the file itself.
|
22
|
+
class DynamicFileTask < Rake::FileTask
|
23
|
+
attr_writer :last_manifest
|
24
|
+
attr_writer :manifest
|
25
|
+
|
26
|
+
# @return [Boolean] true if the task has a block to invoke
|
27
|
+
# for dynamic dependencies, false otherwise.
|
28
|
+
def has_dynamic_block?
|
29
|
+
!!@dynamic
|
30
|
+
end
|
31
|
+
|
32
|
+
def last_manifest
|
33
|
+
@last_manifest ||= Rake::Pipeline::Manifest.new
|
34
|
+
end
|
35
|
+
|
36
|
+
def manifest
|
37
|
+
@manifest ||= Rake::Pipeline::Manifest.new
|
38
|
+
end
|
39
|
+
|
40
|
+
# @return [ManifestEntry] the manifest entry from the last time
|
41
|
+
# this task was run, usually read off the filesystem.
|
42
|
+
def last_manifest_entry
|
43
|
+
last_manifest[name]
|
44
|
+
end
|
45
|
+
|
46
|
+
# @return [ManifestEntry] the manifest entry from the current
|
47
|
+
# manifest. This is the entry that will be written to disk after
|
48
|
+
# the task runs.
|
49
|
+
def manifest_entry
|
50
|
+
manifest[name]
|
51
|
+
end
|
52
|
+
|
53
|
+
# Set the current manifest entry,
|
54
|
+
#
|
55
|
+
# @param [ManifestEntry] new_entry
|
56
|
+
# @return [ManifestEntry]
|
57
|
+
def manifest_entry=(new_entry)
|
58
|
+
manifest[name] = new_entry
|
59
|
+
end
|
60
|
+
|
61
|
+
# In addition to the regular FileTask check, A DynamicFileTask is
|
62
|
+
# needed if it has no manifest entry from a previous run, or if
|
63
|
+
# one of its dynamic dependencies has been modified.
|
64
|
+
#
|
65
|
+
# @return [Boolean]
|
66
|
+
def needed?
|
67
|
+
return true if super
|
68
|
+
|
69
|
+
# if we have no manifest, this file task is needed
|
70
|
+
return true unless last_manifest_entry
|
71
|
+
|
72
|
+
# If any of this task's dynamic dependencies have changed,
|
73
|
+
# this file task is needed
|
74
|
+
last_manifest_entry.deps.each do |dep, time|
|
75
|
+
return true if File.mtime(dep) > time
|
76
|
+
end
|
77
|
+
|
78
|
+
# Otherwise, it's not needed
|
79
|
+
false
|
80
|
+
end
|
81
|
+
|
82
|
+
# Add a block that will return dynamic dependencies. This
|
83
|
+
# block can assume that all static dependencies are up
|
84
|
+
# to date.
|
85
|
+
#
|
86
|
+
# @return [DynamicFileTask] self
|
87
|
+
def dynamic(&block)
|
88
|
+
@dynamic = block
|
89
|
+
self
|
90
|
+
end
|
91
|
+
|
92
|
+
# Invoke the task's dynamic block.
|
93
|
+
def invoke_dynamic_block
|
94
|
+
@dynamic.call(self)
|
95
|
+
end
|
96
|
+
|
97
|
+
# At runtime, we will call this to get dynamic prerequisites.
|
98
|
+
#
|
99
|
+
# @return [Array[String]] an array of paths to the task's
|
100
|
+
# dynamic dependencies.
|
101
|
+
def dynamic_prerequisites
|
102
|
+
@dynamic_prerequisites ||= begin
|
103
|
+
dynamics = if has_dynamic_block?
|
104
|
+
dynamic_prerequisites_from_manifest || invoke_dynamic_block
|
105
|
+
else
|
106
|
+
[]
|
107
|
+
end
|
108
|
+
|
109
|
+
# Make sure we don't dynamically depend on ourselves, as
|
110
|
+
# that will create a circular reference, and that makes
|
111
|
+
# everybody sad.
|
112
|
+
dynamics.reject { |x| x == name }
|
113
|
+
end
|
114
|
+
end
|
115
|
+
|
116
|
+
# Override rake's invoke_prerequisites method to invoke
|
117
|
+
# static prerequisites and then any dynamic prerequisites.
|
118
|
+
def invoke_prerequisites(task_args, invocation_chain)
|
119
|
+
super
|
120
|
+
|
121
|
+
# If we don't have a dynamic block, just act like a regular FileTask.
|
122
|
+
return unless has_dynamic_block?
|
123
|
+
|
124
|
+
# Retrieve the dynamic prerequisites. If all goes well,
|
125
|
+
# we will not have to invoke the dynamic block to do this.
|
126
|
+
dynamics = dynamic_prerequisites
|
127
|
+
|
128
|
+
# invoke dynamic prerequisites just as we would invoke
|
129
|
+
# static prerequisites.
|
130
|
+
dynamics.each do |prereq|
|
131
|
+
task = lookup_prerequisite(prereq)
|
132
|
+
prereq_args = task_args.new_scope(task.arg_names)
|
133
|
+
task.invoke_with_call_chain(prereq_args, invocation_chain)
|
134
|
+
end
|
135
|
+
|
136
|
+
# Create a new manifest entry for each dynamic dependency.
|
137
|
+
# When the pipeline finishes, these manifest entries will be written
|
138
|
+
# to the file system.
|
139
|
+
entry = Rake::Pipeline::ManifestEntry.new
|
140
|
+
|
141
|
+
dynamics.each do |dynamic|
|
142
|
+
entry.deps.merge!(dynamic => mtime_or_now(dynamic))
|
143
|
+
end
|
144
|
+
|
145
|
+
self.manifest_entry = entry
|
146
|
+
end
|
147
|
+
|
148
|
+
# After invoking a task, add the mtime of the task's output
|
149
|
+
# to its current manifest entry.
|
150
|
+
def invoke_with_call_chain(*)
|
151
|
+
super
|
152
|
+
return unless has_dynamic_block?
|
153
|
+
|
154
|
+
manifest_entry.mtime = mtime_or_now(name)
|
155
|
+
end
|
156
|
+
|
157
|
+
private
|
158
|
+
# @return the mtime of the given file if it exists, and
|
159
|
+
# the current time otherwise.
|
160
|
+
def mtime_or_now(filename)
|
161
|
+
File.file?(filename) ? File.mtime(filename) : Time.now
|
162
|
+
end
|
163
|
+
|
164
|
+
# @return [Array<String>] a list of file paths that this
|
165
|
+
# task depends on.
|
166
|
+
# @return [nil] if the dependencies couldn't be read
|
167
|
+
# from the manifest.
|
168
|
+
def dynamic_prerequisites_from_manifest
|
169
|
+
# Try to avoid invoking the dynamic block if this file
|
170
|
+
# is not needed. If so, we may have all the information
|
171
|
+
# we need in the manifest file.
|
172
|
+
if !needed? && last_manifest_entry
|
173
|
+
mtime = last_manifest_entry.mtime
|
174
|
+
end
|
175
|
+
|
176
|
+
# If the output file of this task still exists and
|
177
|
+
# it hasn't been updated, we can simply return the
|
178
|
+
# list of dependencies in the manifest, which
|
179
|
+
# come from the return value of the dynamic block
|
180
|
+
# in a previous run.
|
181
|
+
if File.exist?(name) && mtime == File.mtime(name)
|
182
|
+
return last_manifest_entry.deps.map { |k,v| k }
|
183
|
+
end
|
184
|
+
end
|
185
|
+
end
|
186
|
+
end
|
187
|
+
end
|
188
|
+
|
data/lib/rake-pipeline/filter.rb
CHANGED
@@ -46,7 +46,9 @@ module Rake
|
|
46
46
|
attr_accessor :input_files
|
47
47
|
|
48
48
|
# @return [Proc] a block that returns the relative output
|
49
|
-
# filename for a particular input file.
|
49
|
+
# filename for a particular input file. If the block accepts
|
50
|
+
# just one argument, it will be passed the input's path. If
|
51
|
+
# it accepts two, it will also be passed the input itself.
|
50
52
|
attr_accessor :output_name_generator
|
51
53
|
|
52
54
|
# @return [String] the root directory to write output files
|
@@ -66,6 +68,10 @@ module Rake
|
|
66
68
|
# filter should define new rake tasks on.
|
67
69
|
attr_writer :rake_application
|
68
70
|
|
71
|
+
# @return [Rake::Pipeline] the Rake::Pipeline that contains
|
72
|
+
# this filter.
|
73
|
+
attr_accessor :pipeline
|
74
|
+
|
69
75
|
attr_writer :file_wrapper_class
|
70
76
|
|
71
77
|
# @param [Proc] block a block to use as the Filter's
|
@@ -73,6 +79,7 @@ module Rake
|
|
73
79
|
def initialize(&block)
|
74
80
|
block ||= proc { |input| input }
|
75
81
|
@output_name_generator = block
|
82
|
+
@input_files = []
|
76
83
|
end
|
77
84
|
|
78
85
|
# Invoke this method in a subclass of Filter to declare that
|
@@ -148,10 +155,10 @@ module Rake
|
|
148
155
|
hash = {}
|
149
156
|
|
150
157
|
input_files.each do |file|
|
151
|
-
|
152
|
-
|
153
|
-
|
154
|
-
|
158
|
+
output_wrappers(file).each do |output|
|
159
|
+
hash[output] ||= []
|
160
|
+
hash[output] << file
|
161
|
+
end
|
155
162
|
end
|
156
163
|
|
157
164
|
hash
|
@@ -163,9 +170,7 @@ module Rake
|
|
163
170
|
# @see #outputs
|
164
171
|
# @return [Array<FileWrapper>]
|
165
172
|
def output_files
|
166
|
-
input_files.
|
167
|
-
array |= [output_wrapper(output_name_generator.call(file.path))]
|
168
|
-
end
|
173
|
+
input_files.map { |file| output_wrappers(file) }.flatten.uniq
|
169
174
|
end
|
170
175
|
|
171
176
|
# The Rake::Application that the filter should define new tasks on.
|
@@ -175,17 +180,27 @@ module Rake
|
|
175
180
|
@rake_application || Rake.application
|
176
181
|
end
|
177
182
|
|
183
|
+
# @param [FileWrapper] file wrapper to get paths for
|
184
|
+
# @return [Array<String>] array of file paths within additional dependencies
|
185
|
+
def additional_dependencies(input)
|
186
|
+
[]
|
187
|
+
end
|
188
|
+
|
178
189
|
# Generate the Rake tasks for the output files of this filter.
|
179
190
|
#
|
180
191
|
# @see #outputs #outputs (for information on how the output files are determined)
|
181
192
|
# @return [void]
|
182
193
|
def generate_rake_tasks
|
183
194
|
@rake_tasks = outputs.map do |output, inputs|
|
184
|
-
|
185
|
-
|
186
|
-
|
195
|
+
additional_paths = []
|
196
|
+
inputs.each do |input|
|
197
|
+
create_file_task(input.fullpath).dynamic do
|
198
|
+
additional_paths += additional_dependencies(input)
|
199
|
+
end
|
200
|
+
end
|
201
|
+
additional_paths.each { |path| create_file_task(path) }
|
187
202
|
|
188
|
-
create_file_task(output.fullpath,
|
203
|
+
create_file_task(output.fullpath, inputs.map(&:fullpath)) do
|
189
204
|
output.create { generate_output(inputs, output) }
|
190
205
|
end
|
191
206
|
end
|
@@ -198,11 +213,26 @@ module Rake
|
|
198
213
|
end
|
199
214
|
|
200
215
|
def create_file_task(output, deps=[], &block)
|
201
|
-
rake_application.define_task(Rake::
|
216
|
+
task = rake_application.define_task(Rake::Pipeline::DynamicFileTask, output => deps, &block)
|
217
|
+
|
218
|
+
if pipeline && pipeline.project
|
219
|
+
task.last_manifest = pipeline.project.last_manifest
|
220
|
+
task.manifest = pipeline.project.manifest
|
221
|
+
end
|
222
|
+
|
223
|
+
task
|
224
|
+
end
|
225
|
+
|
226
|
+
def output_wrappers(input)
|
227
|
+
output_paths(input).map do |path|
|
228
|
+
file_wrapper_class.new(output_root, path, encoding)
|
229
|
+
end
|
202
230
|
end
|
203
231
|
|
204
|
-
def
|
205
|
-
|
232
|
+
def output_paths(input)
|
233
|
+
args = [ input.path ]
|
234
|
+
args << input if output_name_generator.arity == 2
|
235
|
+
Array(output_name_generator.call(*args))
|
206
236
|
end
|
207
237
|
end
|
208
238
|
end
|
File without changes
|
@@ -0,0 +1,38 @@
|
|
1
|
+
class Rake::Pipeline
|
2
|
+
# A filter that concats files in a specified order.
|
3
|
+
#
|
4
|
+
# @example
|
5
|
+
# !!!ruby
|
6
|
+
# Rake::Pipeline.build do
|
7
|
+
# input "app/assets", "**/*.js"
|
8
|
+
# output "public"
|
9
|
+
#
|
10
|
+
# # Concat each file into libs.js but make sure
|
11
|
+
# # that jQuery and Ember come first.
|
12
|
+
# filter Rake::Pipeline::OrderingConcatFilter, ["jquery.js", "ember.js"], "libs.js"
|
13
|
+
# end
|
14
|
+
class OrderingConcatFilter < ConcatFilter
|
15
|
+
|
16
|
+
# @param [Array<String>] ordering an Array of Strings
|
17
|
+
# of file names that should come in the specified order
|
18
|
+
# @param [String] string the name of the output file to
|
19
|
+
# concatenate inputs to.
|
20
|
+
# @param [Proc] block a block to use as the Filter's
|
21
|
+
# {#output_name_generator}.
|
22
|
+
def initialize(ordering, string=nil, &block)
|
23
|
+
@ordering = ordering
|
24
|
+
super(string, &block)
|
25
|
+
end
|
26
|
+
|
27
|
+
# Extend the {#generate_output} method supplied by {ConcatFilter}.
|
28
|
+
# Re-orders the inputs such that the specified files come first.
|
29
|
+
# If a file is not in the list it will come after the specified files.
|
30
|
+
def generate_output(inputs, output)
|
31
|
+
@ordering.reverse.each do |name|
|
32
|
+
file = inputs.find{|i| i.path == name }
|
33
|
+
inputs.unshift(inputs.delete(file)) if file
|
34
|
+
end
|
35
|
+
super
|
36
|
+
end
|
37
|
+
end
|
38
|
+
end
|
@@ -0,0 +1,19 @@
|
|
1
|
+
module Rake
|
2
|
+
class Pipeline
|
3
|
+
# @private
|
4
|
+
#
|
5
|
+
# A built-in filter that copies a pipeline's generated files over
|
6
|
+
# to its output.
|
7
|
+
class PipelineFinalizingFilter < ConcatFilter
|
8
|
+
|
9
|
+
# @return [Array[FileWrapper]] a list of the pipeline's
|
10
|
+
# output files, excluding any files that were originally
|
11
|
+
# inputs to the pipeline, meaning they weren't processed
|
12
|
+
# by any filter and should not be copied to the output.
|
13
|
+
def input_files
|
14
|
+
pipeline_input_files = pipeline.input_files
|
15
|
+
super.reject { |file| pipeline_input_files.include?(file) }
|
16
|
+
end
|
17
|
+
end
|
18
|
+
end
|
19
|
+
end
|
@@ -0,0 +1,178 @@
|
|
1
|
+
require "set"
|
2
|
+
|
3
|
+
module Rake
|
4
|
+
class Pipeline
|
5
|
+
# The goal of this class is to make is easy to implement dynamic
|
6
|
+
# dependencies in additional_dependencies without having to parse
|
7
|
+
# all the files all of the time.
|
8
|
+
#
|
9
|
+
# To illustrate, imagine that we have two source files with the
|
10
|
+
# following inline dependencies:
|
11
|
+
#
|
12
|
+
# * application.scss
|
13
|
+
# * _core.scss
|
14
|
+
# * admin.scss
|
15
|
+
# * _admin.scss
|
16
|
+
#
|
17
|
+
# And further imagine that `_admin.scss` has an inline dependency
|
18
|
+
# on `_core.scss`.
|
19
|
+
#
|
20
|
+
# On initial build, we will scan all of the source files, find
|
21
|
+
# the dependencies, and build a node for each file, annotating
|
22
|
+
# the source files with `:source => true`. We also store off the
|
23
|
+
# `mtime` of each file in its node. We link each file to its
|
24
|
+
# dependencies.
|
25
|
+
#
|
26
|
+
# The `additional_dependencies` are a map of the files to their
|
27
|
+
# children, which will be used when generating rake tasks.
|
28
|
+
#
|
29
|
+
# Later, let's say that we change `_admin.scss`. We will need
|
30
|
+
# to unlink its dependencies first (on `_core.scss`), rescan
|
31
|
+
# the file, and create nodes for its dependencies. If no new
|
32
|
+
# dependencies
|
33
|
+
|
34
|
+
class Graph
|
35
|
+
class MissingNode < StandardError
|
36
|
+
end
|
37
|
+
|
38
|
+
class Node
|
39
|
+
# @return [String] the identifier of the node
|
40
|
+
attr_reader :name
|
41
|
+
|
42
|
+
# @return [Set] a Set of parent nodes
|
43
|
+
attr_reader :parents
|
44
|
+
|
45
|
+
# @return [Set] a Set of child nodes
|
46
|
+
attr_reader :children
|
47
|
+
|
48
|
+
# @return [Hash] a Hash of metadata
|
49
|
+
attr_reader :metadata
|
50
|
+
|
51
|
+
# @param [String] name the identifier of the node
|
52
|
+
# @param [Hash] metadata an optional hash of metadata
|
53
|
+
def initialize(name, metadata={})
|
54
|
+
@name = name
|
55
|
+
@parents = Set.new
|
56
|
+
@children = Set.new
|
57
|
+
@metadata = metadata
|
58
|
+
end
|
59
|
+
|
60
|
+
# A node is equal another node if it has the same name.
|
61
|
+
# This is because the Graph ensures that only one node
|
62
|
+
# with a given name can be created.
|
63
|
+
#
|
64
|
+
# @param [Node] other the node to compare
|
65
|
+
def ==(other)
|
66
|
+
@name == other.name
|
67
|
+
end
|
68
|
+
end
|
69
|
+
|
70
|
+
def initialize
|
71
|
+
@map = {}
|
72
|
+
end
|
73
|
+
|
74
|
+
# @return [Array] an Array of all of the nodes in the graph
|
75
|
+
def nodes
|
76
|
+
@map.values
|
77
|
+
end
|
78
|
+
|
79
|
+
# Add a new node to the graph. If an existing node with the
|
80
|
+
# current name already exists, do not add the node.
|
81
|
+
#
|
82
|
+
# @param [String] name an identifier for the node.
|
83
|
+
# @param [Hash] metadata optional metadata for the node
|
84
|
+
def add(name, metadata={})
|
85
|
+
return if @map.include?(name)
|
86
|
+
@map[name] = Node.new(name, metadata)
|
87
|
+
end
|
88
|
+
|
89
|
+
# Remove a node from the graph. Unlink its parent and children
|
90
|
+
# from it.
|
91
|
+
#
|
92
|
+
# If the existing node does not exist, raise.
|
93
|
+
#
|
94
|
+
# @param [String] name an identifier for the node
|
95
|
+
def remove(name)
|
96
|
+
node = verify(name)
|
97
|
+
|
98
|
+
node.parents.each do |parent_node|
|
99
|
+
parent_node.children.delete node
|
100
|
+
end
|
101
|
+
|
102
|
+
node.children.each do |child_node|
|
103
|
+
child_node.parents.delete node
|
104
|
+
end
|
105
|
+
|
106
|
+
@map.delete(name)
|
107
|
+
end
|
108
|
+
|
109
|
+
# Add a link from the parent to the child. This link is a
|
110
|
+
# two-way link, so the child will be added to the parent's
|
111
|
+
# `children` and the parent will be added to the child's
|
112
|
+
# `parents`.
|
113
|
+
#
|
114
|
+
# The parent and child are referenced by node identifier.
|
115
|
+
#
|
116
|
+
# @param [String] parent the identifier of the parent
|
117
|
+
# @param [String] child the identifier of the child
|
118
|
+
def link(parent, child)
|
119
|
+
parent, child = lookup(parent, child)
|
120
|
+
|
121
|
+
parent.children << child
|
122
|
+
child.parents << parent
|
123
|
+
end
|
124
|
+
|
125
|
+
# Remove a link from the parent to the child.
|
126
|
+
#
|
127
|
+
# The parent and child are referenced by node identifier.
|
128
|
+
#
|
129
|
+
# @param [String] parent the identifier of the parent
|
130
|
+
# @param [String] child the identifier of the child
|
131
|
+
def unlink(parent, child)
|
132
|
+
parent, child = lookup(parent, child)
|
133
|
+
|
134
|
+
parent.children.delete(child)
|
135
|
+
child.parents.delete(parent)
|
136
|
+
end
|
137
|
+
|
138
|
+
# Look up a node by name
|
139
|
+
#
|
140
|
+
# @param [String] name the identifier of the node
|
141
|
+
# @return [Node] the node referenced by the specified identifier
|
142
|
+
def [](name)
|
143
|
+
@map[name]
|
144
|
+
end
|
145
|
+
|
146
|
+
private
|
147
|
+
# Verify that the parent and child nodes exist, and return
|
148
|
+
# the nodes with the specified identifiers.
|
149
|
+
#
|
150
|
+
# The parent and child are referenced by node identifier.
|
151
|
+
#
|
152
|
+
# @param [String] parent the identifier of the parent
|
153
|
+
# @param [String] child the identifier of the child
|
154
|
+
# @return [Array(Node, Node)] the parent and child nodes
|
155
|
+
def lookup(parent, child)
|
156
|
+
parent = verify(parent)
|
157
|
+
child = verify(child)
|
158
|
+
|
159
|
+
return parent, child
|
160
|
+
end
|
161
|
+
|
162
|
+
# Verify that a node with a given identifier exists, and
|
163
|
+
# if it does, return it.
|
164
|
+
#
|
165
|
+
# If it does not, raise an exception.
|
166
|
+
#
|
167
|
+
# @param [String] name the identifier of the node
|
168
|
+
# @raise [MissingNode] if a node with the given name is
|
169
|
+
# not found, raise.
|
170
|
+
# @return [Node] the n
|
171
|
+
def verify(name)
|
172
|
+
node = @map[name]
|
173
|
+
raise MissingNode, "Node #{name} does not exist" unless node
|
174
|
+
node
|
175
|
+
end
|
176
|
+
end
|
177
|
+
end
|
178
|
+
end
|