rake-pipeline 0.5.0 → 0.7.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/.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
|