prick 0.2.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +7 -0
- data/.gitignore +28 -0
- data/.rspec +3 -0
- data/.ruby-version +1 -0
- data/.travis.yml +7 -0
- data/Gemfile +6 -0
- data/README.md +35 -0
- data/Rakefile +6 -0
- data/bin/console +14 -0
- data/bin/setup +8 -0
- data/doc/create_release.txt +17 -0
- data/doc/flow.txt +98 -0
- data/doc/migra +1 -0
- data/doc/migrations.txt +172 -0
- data/doc/notes.txt +116 -0
- data/doc/sh.prick +316 -0
- data/exe/prick +467 -0
- data/file +0 -0
- data/lib/ext/algorithm.rb +14 -0
- data/lib/ext/fileutils.rb +8 -0
- data/lib/ext/pg.rb +18 -0
- data/lib/prick.rb +21 -0
- data/lib/prick/archive.rb +124 -0
- data/lib/prick/build.rb +376 -0
- data/lib/prick/command.rb +85 -0
- data/lib/prick/constants.rb +199 -0
- data/lib/prick/database.rb +58 -0
- data/lib/prick/dsort.rb +151 -0
- data/lib/prick/ensure.rb +119 -0
- data/lib/prick/exceptions.rb +13 -0
- data/lib/prick/git.rb +159 -0
- data/lib/prick/migra.rb +22 -0
- data/lib/prick/migration.rb +230 -0
- data/lib/prick/project.rb +444 -0
- data/lib/prick/rdbms.rb +147 -0
- data/lib/prick/schema.rb +100 -0
- data/lib/prick/version.rb +133 -0
- data/make_releases +369 -0
- data/prick.gemspec +46 -0
- data/share/features/diff.sql +2 -0
- data/share/features/feature/diff.sql +2 -0
- data/share/features/feature/migrate.sql +2 -0
- data/share/features/features.sql +2 -0
- data/share/features/features.yml +2 -0
- data/share/features/migrations.sql +4 -0
- data/share/gitignore +2 -0
- data/share/schemas/prick/data.sql +8 -0
- data/share/schemas/prick/schema.sql +20 -0
- metadata +188 -0
@@ -0,0 +1,85 @@
|
|
1
|
+
require 'fcntl'
|
2
|
+
|
3
|
+
module Command
|
4
|
+
class Error < RuntimeError
|
5
|
+
attr_reader :status
|
6
|
+
attr_reader :stdout
|
7
|
+
attr_reader :stderr
|
8
|
+
|
9
|
+
def initialize(message, status, stdout, stderr)
|
10
|
+
super(message)
|
11
|
+
@status = status
|
12
|
+
@stdout = stdout
|
13
|
+
@stderr = stderr
|
14
|
+
end
|
15
|
+
end
|
16
|
+
|
17
|
+
# Execute the shell command 'cmd' and return the output as an array of
|
18
|
+
# strings
|
19
|
+
#
|
20
|
+
# If :stderr is true, it returns return a tuple of [stdout, stderr] instead
|
21
|
+
#
|
22
|
+
# The shell command is executed with the `errexit` and `pipefail` bash
|
23
|
+
# options. It raises a Command::Error exception if the command fails unless
|
24
|
+
# :fail is true. The exit status of the last command is stored in ::status
|
25
|
+
#
|
26
|
+
def command(cmd, stderr: false, fail: true)
|
27
|
+
cmd = "set -o errexit\nset -o pipefail\n#{cmd}"
|
28
|
+
|
29
|
+
pw = IO::pipe # pipe[0] for read, pipe[1] for write
|
30
|
+
pr = IO::pipe
|
31
|
+
pe = IO::pipe
|
32
|
+
|
33
|
+
STDOUT.flush
|
34
|
+
|
35
|
+
pid = fork {
|
36
|
+
pw[1].close
|
37
|
+
pr[0].close
|
38
|
+
pe[0].close
|
39
|
+
|
40
|
+
STDIN.reopen(pw[0])
|
41
|
+
pw[0].close
|
42
|
+
|
43
|
+
STDOUT.reopen(pr[1])
|
44
|
+
pr[1].close
|
45
|
+
|
46
|
+
STDERR.reopen(pe[1])
|
47
|
+
pe[1].close
|
48
|
+
|
49
|
+
exec(cmd)
|
50
|
+
}
|
51
|
+
|
52
|
+
pw[0].close
|
53
|
+
pr[1].close
|
54
|
+
pe[1].close
|
55
|
+
|
56
|
+
@status = Process.waitpid2(pid)[1].exitstatus
|
57
|
+
|
58
|
+
out = pr[0].readlines.collect { |line| line.chop }
|
59
|
+
err = pe[0].readlines.collect { |line| line.chop }
|
60
|
+
|
61
|
+
pw[1].close
|
62
|
+
pr[0].close
|
63
|
+
pe[0].close
|
64
|
+
|
65
|
+
if @status == 0 || fail == false
|
66
|
+
stderr ? [out, err] : out
|
67
|
+
else
|
68
|
+
raise Command::Error.new((out + err).join("\n") + "\n", status, out, err)
|
69
|
+
end
|
70
|
+
end
|
71
|
+
|
72
|
+
# Exit status of the last command
|
73
|
+
def status() @status end
|
74
|
+
|
75
|
+
# Like command but returns true if the command exited with the expected status
|
76
|
+
def command?(cmd, expect: 0)
|
77
|
+
command(cmd, fail: false)
|
78
|
+
@status == expect
|
79
|
+
end
|
80
|
+
|
81
|
+
module_function :command
|
82
|
+
module_function :status
|
83
|
+
module_function :command?
|
84
|
+
end
|
85
|
+
|
@@ -0,0 +1,199 @@
|
|
1
|
+
|
2
|
+
module Prick
|
3
|
+
# Shared files (part of the installation)
|
4
|
+
SHARE_PATH = "#{File.dirname(File.dirname(__dir__))}/share"
|
5
|
+
|
6
|
+
# Project directories
|
7
|
+
DIRS = [
|
8
|
+
RELEASE_DIR = "releases",
|
9
|
+
MIGRATION_DIR = "migrations",
|
10
|
+
FEATURE_DIR = "features",
|
11
|
+
SCHEMA_DIR = "schemas",
|
12
|
+
PRICK_DIR = "#{SCHEMA_DIR}/prick",
|
13
|
+
PUBLIC_DIR = "#{SCHEMA_DIR}/public",
|
14
|
+
VAR_DIR = "var",
|
15
|
+
CACHE_DIR = "#{VAR_DIR}/cache",
|
16
|
+
TMP_DIR = "tmp",
|
17
|
+
CLONE_DIR = "tmp/clone",
|
18
|
+
SPEC_DIR = "spec"
|
19
|
+
]
|
20
|
+
|
21
|
+
# Dump files
|
22
|
+
DUMP_EXT = "dump.gz"
|
23
|
+
DUMP_GLOB = "*-[0-9]*.#{DUMP_EXT}"
|
24
|
+
def self.dump_glob(project_name) "#{project_name}-*.#{DUMP_EXT}" end
|
25
|
+
|
26
|
+
|
27
|
+
# Matches an identifier. Identifiers consist of lower case letters, digits
|
28
|
+
# and underscores but not dashes because they're used as separators
|
29
|
+
IDENT_SUB_RE = /[a-z][a-z0-9_]*/
|
30
|
+
IDENT_RE = /^#{IDENT_SUB_RE}$/
|
31
|
+
|
32
|
+
# Matches an uppercase identifier
|
33
|
+
# UPCASE_IDENT_SUB_RE = /[A-Z][A-Z0-9_]*/
|
34
|
+
# UPCASE_IDENT_RE = /#{UPCASE_IDENT_SUB_RE}/
|
35
|
+
|
36
|
+
# A (system) name. Names are used for projects and usernames that are
|
37
|
+
# external to prick and can include both dashes and underscores but dashes
|
38
|
+
# have to be followed by a character and not a digit so 'ident-1234' is not
|
39
|
+
# allowed but 'ident_1234' and 'ident1234' are
|
40
|
+
NAME_SUB_RE = /#{IDENT_SUB_RE}/
|
41
|
+
NAME_RE = /^#{NAME_SUB_RE}$/
|
42
|
+
|
43
|
+
# Matches a project name
|
44
|
+
PROJECT_NAME_SUB_RE = NAME_SUB_RE
|
45
|
+
PROJECT_NAME_RE = NAME_RE
|
46
|
+
|
47
|
+
# Matches a custom name
|
48
|
+
CUSTOM_NAME_SUB_RE = NAME_SUB_RE
|
49
|
+
CUSTOM_NAME_RE = NAME_RE
|
50
|
+
|
51
|
+
# Matches a feature name
|
52
|
+
FEATURE_NAME_SUB_RE = NAME_SUB_RE
|
53
|
+
FEATURE_NAME_RE = NAME_RE
|
54
|
+
|
55
|
+
# Matches a postgres user name
|
56
|
+
USER_NAME_SUB_RE = NAME_SUB_RE
|
57
|
+
USER_NAME_RE = NAME_RE
|
58
|
+
|
59
|
+
# Matches a major.minor.patch version
|
60
|
+
#
|
61
|
+
# The *_SEMVER REs are derived from the canonical RE
|
62
|
+
#
|
63
|
+
# /
|
64
|
+
# (0|[1-9]\d*)\.(0|[1-9]\d*)\.(0|[1-9]\d*)
|
65
|
+
# (?:-((?:0|[1-9]\d*|\d*[a-zA-Z-][0-9a-zA-Z-]*)(?:\.(?:0|[1-9]\d*|\d*[a-zA-Z-][0-9a-zA-Z-]*))*))?
|
66
|
+
# (?:\+([0-9a-zA-Z-]+(?:\.[0-9a-zA-Z-]+)*))?
|
67
|
+
# /x
|
68
|
+
#
|
69
|
+
# https://semver.org/#is-there-a-suggested-regular-expression-regex-to-check-a-semver-string.
|
70
|
+
#
|
71
|
+
MMP_SEMVER_SUB_RE = /(?:0|[1-9]\d*)\.(?:0|[1-9]\d*)\.(?:0|[1-9]\d*)/
|
72
|
+
MMP_SEMVER_RE = /^#{MMP_SEMVER_SUB_RE}$/
|
73
|
+
|
74
|
+
# Matches the prelease part of a semantic version
|
75
|
+
#
|
76
|
+
PRE_SEMVER_SUB_RE = /pre\.\d+/
|
77
|
+
PRE_SEMVER_RE = /^#{PRE_SEMVER_SUB_RE}$/
|
78
|
+
|
79
|
+
# Matches a semantic version
|
80
|
+
#
|
81
|
+
SEMVER_SUB_RE = /#{MMP_SEMVER_SUB_RE}(?:-#{PRE_SEMVER_SUB_RE})?/
|
82
|
+
SEMVER_RE = /^#{SEMVER_SUB_RE}$/
|
83
|
+
|
84
|
+
# Version RE. The general syntax for a version is '<custom>-<version>_<feature>'
|
85
|
+
#
|
86
|
+
# The RE defines the following captures:
|
87
|
+
# $1 - custom name, can be nil
|
88
|
+
# $2 - semantic version
|
89
|
+
# $3 - feature name, can be nil
|
90
|
+
#
|
91
|
+
VERSION_SUB_RE = /
|
92
|
+
(?:(#{CUSTOM_NAME_SUB_RE})-)?
|
93
|
+
(#{SEMVER_SUB_RE})
|
94
|
+
(?:_(#{FEATURE_NAME_SUB_RE}))?
|
95
|
+
/x
|
96
|
+
VERSION_RE = /^#{VERSION_SUB_RE}$/
|
97
|
+
|
98
|
+
# Matches an abstract release (either a release or a prerelease)
|
99
|
+
#
|
100
|
+
# The RE defines the following captures:
|
101
|
+
# $1 - custom name, can be nil
|
102
|
+
# $2 - semantic version
|
103
|
+
#
|
104
|
+
ABSTRACT_RELEASE_SUB_RE = /
|
105
|
+
(?:(#{CUSTOM_NAME_SUB_RE})-)?
|
106
|
+
(#{SEMVER_SUB_RE})
|
107
|
+
/x
|
108
|
+
ABSTRACT_RELEASE_RE = /^#{ABSTRACT_RELEASE_SUB_RE}$/
|
109
|
+
|
110
|
+
# Matches a (proper) release
|
111
|
+
#
|
112
|
+
# The RE defines the following captures:
|
113
|
+
# $1 - custom name, can be nil
|
114
|
+
# $2 - semantic version
|
115
|
+
#
|
116
|
+
RELEASE_SUB_RE = /
|
117
|
+
(?:(#{CUSTOM_NAME_SUB_RE})-)?
|
118
|
+
(#{MMP_SEMVER_SUB_RE})
|
119
|
+
/x
|
120
|
+
RELEASE_RE = /^#{RELEASE_SUB_RE}$/
|
121
|
+
|
122
|
+
# Migration RE. The syntax is <version>_<version>
|
123
|
+
#
|
124
|
+
# The RE defines the following captures
|
125
|
+
# $1 - from version
|
126
|
+
# $2 - from custom name, can be nil
|
127
|
+
# $3 - from semantic version
|
128
|
+
# $4 - to version
|
129
|
+
# $5 - to custom name, can be nil
|
130
|
+
# $6 - to semantic version
|
131
|
+
#
|
132
|
+
MIGRATION_SUB_RE = /(#{RELEASE_SUB_RE})_(#{RELEASE_SUB_RE})/
|
133
|
+
MIGRATION_RE = /^#{MIGRATION_SUB_RE}$/
|
134
|
+
|
135
|
+
# Matches a prerelease branch
|
136
|
+
#
|
137
|
+
# The RE defines the following captures:
|
138
|
+
# $1 - custom name, can be nil
|
139
|
+
# $2 - semantic version
|
140
|
+
#
|
141
|
+
PRERELEASE_SUB_RE = /
|
142
|
+
(?:(#{CUSTOM_NAME_SUB_RE})-)?
|
143
|
+
(#{MMP_SEMVER_SUB_RE}-#{PRE_SEMVER_SUB_RE})
|
144
|
+
/x
|
145
|
+
PRERELEASE_RE = /^#{PRERELEASE_SUB_RE}$/
|
146
|
+
|
147
|
+
# Matches a feature branch
|
148
|
+
#
|
149
|
+
# The RE defines the following captures:
|
150
|
+
# $1 - custom name, can be nil
|
151
|
+
# $2 - semantic version
|
152
|
+
# $3 - feature name
|
153
|
+
#
|
154
|
+
FEATURE_SUB_RE = /#{ABSTRACT_RELEASE_SUB_RE}_(#{FEATURE_NAME_SUB_RE})/
|
155
|
+
FEATURE_RE = /^#{FEATURE_SUB_RE}$/
|
156
|
+
|
157
|
+
# Project release RE. The general syntax is '<project>-<custom>-<version>'
|
158
|
+
#
|
159
|
+
# The RE defines the following captures:
|
160
|
+
# $1 - project
|
161
|
+
# $2 - version
|
162
|
+
# $3 - custom name, can be nil
|
163
|
+
# $4 - semantic version
|
164
|
+
#
|
165
|
+
PROJECT_SUB_RE = /(#{PROJECT_NAME_SUB_RE})-(#{ABSTRACT_RELEASE_SUB_RE})/
|
166
|
+
PROJECT_RE = /^#{PROJECT_SUB_RE}$/
|
167
|
+
def self.project_sub_re(project_name)
|
168
|
+
/(#{Regexp.escape(project_name)})(-#{VERSION_SUB_RE})/
|
169
|
+
end
|
170
|
+
def self.release_re(project_name) /^#{project_sub_re(project_name)}$/ end
|
171
|
+
|
172
|
+
# Matches versioned databases. Note that databases never include the feature name.
|
173
|
+
# Features use the project database instead of a feature-specific database
|
174
|
+
#
|
175
|
+
# The RE defines the following captures
|
176
|
+
# $1 - project
|
177
|
+
# $2 - version
|
178
|
+
# $3 - custom name, can be nil
|
179
|
+
# $4 - semantic version
|
180
|
+
#
|
181
|
+
DATABASE_SUB_RE = PROJECT_SUB_RE
|
182
|
+
DATABASE_RE = /^#{DATABASE_SUB_RE}$/
|
183
|
+
def self.database_sub_re(project_name) project_sub_re(project_name) end
|
184
|
+
def self.database_re(project_name) /^#{database_sub_re(project_name)}$/ end
|
185
|
+
|
186
|
+
# Matches project database and versioned databases
|
187
|
+
#
|
188
|
+
# The RE defines the following captures
|
189
|
+
# $1 - project
|
190
|
+
# $2 - version, can be nil
|
191
|
+
# $3 - custom name, can be nil
|
192
|
+
# $4 - semantic version, can be nil
|
193
|
+
#
|
194
|
+
ALL_DATABASES_SUB_RE = /(#{PROJECT_NAME_SUB_RE})(?:-(#{ABSTRACT_RELEASE_SUB_RE}))?/
|
195
|
+
ALL_DATABASES_RE = /^#{ALL_DATABASES_SUB_RE}$/
|
196
|
+
def self.all_databases_sub_re(project_name) /(#{project_name})(?:-(#{ABSTRACT_RELEASE_SUB_RE}))?/ end
|
197
|
+
def self.all_databases_re(project_name) /^#{all_databases_sub_re(project_name)}$/ end
|
198
|
+
end
|
199
|
+
|
@@ -0,0 +1,58 @@
|
|
1
|
+
|
2
|
+
require "prick/ensure.rb"
|
3
|
+
|
4
|
+
module Prick
|
5
|
+
class Database
|
6
|
+
attr_reader :name
|
7
|
+
attr_reader :user
|
8
|
+
|
9
|
+
def initialize(name, user)
|
10
|
+
@name = name
|
11
|
+
@user = user
|
12
|
+
@version = nil
|
13
|
+
end
|
14
|
+
|
15
|
+
def version
|
16
|
+
@version ||= begin
|
17
|
+
version =
|
18
|
+
if exist? && Rdbms.exist_table?(name, "prick", "versions")
|
19
|
+
Rdbms.select(name, "select version from prick.versions")&.first&.first
|
20
|
+
else
|
21
|
+
nil
|
22
|
+
end
|
23
|
+
version && Version.new(version)
|
24
|
+
end
|
25
|
+
end
|
26
|
+
|
27
|
+
def version=(version)
|
28
|
+
@version = version && Version.new(version)
|
29
|
+
Rdbms.exec_sql(name, %(
|
30
|
+
update prick.versions
|
31
|
+
set major = '#{@version.major}',
|
32
|
+
minor = '#{@version.minor}',
|
33
|
+
patch = '#{@version.patch}',
|
34
|
+
pre = '#{@version.pre}',
|
35
|
+
version = '#{@version.to_s}'
|
36
|
+
)) if @version
|
37
|
+
@version
|
38
|
+
end
|
39
|
+
|
40
|
+
def exist?() Rdbms.exist_database?(name) end
|
41
|
+
def create() Rdbms.create_database(name, owner: user) end
|
42
|
+
def recreate() drop if exist?; create; @version = nil end
|
43
|
+
def drop() Rdbms.drop_database(name, fail: false); @version = nil end
|
44
|
+
|
45
|
+
def loaded?() exist? && !version.nil? end
|
46
|
+
def load(file) Rdbms.load(name, file, user: user); @version = nil end
|
47
|
+
|
48
|
+
def save(file) Rdbms.save(name, file); @version = nil end
|
49
|
+
|
50
|
+
include Ensure
|
51
|
+
|
52
|
+
private
|
53
|
+
@states = {
|
54
|
+
exist: [:create, :drop],
|
55
|
+
loaded: [:exist, :load, :recreate]
|
56
|
+
}
|
57
|
+
end
|
58
|
+
end
|
data/lib/prick/dsort.rb
ADDED
@@ -0,0 +1,151 @@
|
|
1
|
+
require 'tsort'
|
2
|
+
|
3
|
+
module DSort
|
4
|
+
# Thrown if a cyclic dependency is detected
|
5
|
+
#
|
6
|
+
# DSort::Cyclic is inherited from TSort::Cyclic so that recue handling code
|
7
|
+
# written for TSort will still work. It provides a #cycles member that lists
|
8
|
+
# the cycles
|
9
|
+
class Cyclic < TSort::Cyclic
|
10
|
+
# List of detected cycles sorted from shortest to longest cycle
|
11
|
+
attr_reader :cycles
|
12
|
+
|
13
|
+
def initialize(dsort_object)
|
14
|
+
@cycles =
|
15
|
+
dsort_object.each_strongly_connected_component \
|
16
|
+
.select { |e| e.size > 1 } \
|
17
|
+
.sort { |l,r| r <=> l }
|
18
|
+
gram = cycles.size > 1 ? "ies" : "y"
|
19
|
+
super("Cyclic depedendenc#{gram} detected")
|
20
|
+
end
|
21
|
+
end
|
22
|
+
|
23
|
+
# dsort sorts its input in "dependency" order: The input can be thought of
|
24
|
+
# depends-on relations between objects and the output as sorted in the order
|
25
|
+
# needed to safisfy those dependencies so that no object comes before an
|
26
|
+
# object it depends on
|
27
|
+
#
|
28
|
+
# dsort can take an array or a hash argument, or be supplied with a block
|
29
|
+
#
|
30
|
+
# The Array argument should consist of pairs (two-element Arrays) with the
|
31
|
+
# first element being the depending object and the second an object or an
|
32
|
+
# array of objects it depends on: For example [:a, :b] means that :a depends
|
33
|
+
# on :b, and [:b, [:c, :d]] that :b depends on both :c and :d
|
34
|
+
#
|
35
|
+
# The Hash argument should be a hash from depending object to an object or
|
36
|
+
# array of objects it depends on. If h is a Hash then dsort(h) is equivalent
|
37
|
+
# to dsort(h.to_a)
|
38
|
+
#
|
39
|
+
# Note that if the elements are arrays themselves, then you should use the
|
40
|
+
# array form to list the dependencies even if there is only one dependency.
|
41
|
+
# Ie. use [:a, [:b]] or {:a => [:b] } instead of [:a, :b] or {:a => :b}
|
42
|
+
#
|
43
|
+
# If dsort is given a block, the block is given an element as argument and
|
44
|
+
# should return an (possibly empty) array of the objects the argument depends
|
45
|
+
# on. The argument to dsort should be an element or an array of elements to
|
46
|
+
# be given to the block. Note that if the elements are arrays themselves,
|
47
|
+
# then the arguments to dsort should use the array form even if there is only
|
48
|
+
# one element. Ie. Use dsort([:a]) instead of dsort(:a)
|
49
|
+
#
|
50
|
+
# dsort raise a DSort::Cyclic exception if a cycle detected
|
51
|
+
#
|
52
|
+
# Example: If we have that dsort depends on ruby and rspec, ruby depends
|
53
|
+
# on C to compile, and rspec depends on ruby, then in what order should we
|
54
|
+
# build them ? Using dsort we could do
|
55
|
+
#
|
56
|
+
# p dsort [[:dsort, [:ruby, :rspec]]], [:ruby, :C], [:rspec, :ruby]]
|
57
|
+
# => [:C, :ruby, :rspec, :dsort]
|
58
|
+
#
|
59
|
+
# Using a hash
|
60
|
+
#
|
61
|
+
# h = {
|
62
|
+
# :dsort => [:ruby, :rspec],
|
63
|
+
# :ruby => [:C],
|
64
|
+
# :rspec => [:ruby]
|
65
|
+
# }
|
66
|
+
# p dsort(h) # Same as dsort(h.to_a)
|
67
|
+
# => [:C, :ruby, :rspec, :dsort]
|
68
|
+
#
|
69
|
+
# or using a block
|
70
|
+
#
|
71
|
+
# p dsort(:dsort) { |e| h[e] }
|
72
|
+
# => [:C, :ruby, :rspec, :dsort]
|
73
|
+
#
|
74
|
+
def dsort(a, &block)
|
75
|
+
sort_object = DSortPrivate::DSortObject.new(a, &block)
|
76
|
+
begin
|
77
|
+
sort_object.tsort
|
78
|
+
rescue TSort::Cyclic
|
79
|
+
raise Cyclic.new(sort_object)
|
80
|
+
end
|
81
|
+
end
|
82
|
+
|
83
|
+
# tsort sort its input in topological order: The input can be thought of as
|
84
|
+
# comes-before relations between objects and the output will be in
|
85
|
+
# first-to-last order. This definition corresponds to the mathemacial
|
86
|
+
# defitionnn of topological sort. See
|
87
|
+
# http://en.wikipedia.org/wiki/Topological_sorting
|
88
|
+
#
|
89
|
+
# Arguments are the same as for dsort. tsort is equivalent to
|
90
|
+
# dsort(...).reverse
|
91
|
+
#
|
92
|
+
# tsort raise a DSort::Cyclic exception if a cycle is detected (DSort::Cyclic
|
93
|
+
# is an alias for TSort::Cyclic)
|
94
|
+
#
|
95
|
+
def tsort(a, &block) dsort(a, &block).reverse end
|
96
|
+
|
97
|
+
module_function :dsort, :tsort
|
98
|
+
|
99
|
+
module DSortPrivate
|
100
|
+
class DSortObject
|
101
|
+
include TSort
|
102
|
+
|
103
|
+
# Hash from element to array of dependencies
|
104
|
+
attr_reader :deps
|
105
|
+
|
106
|
+
# Create @deps hash from object to list of dependencies
|
107
|
+
def initialize(a, &block)
|
108
|
+
@deps = {}
|
109
|
+
if block_given?
|
110
|
+
a = [a] if !a.is_a?(Array)
|
111
|
+
a.each { |elem| find_dependencies(elem, &block) }
|
112
|
+
else
|
113
|
+
a.each { |obj, deps|
|
114
|
+
(@deps[obj] ||= []).concat(deps.is_a?(Array) ? deps : [deps])
|
115
|
+
}
|
116
|
+
end
|
117
|
+
|
118
|
+
# Make sure all dependent objects are also represented as depending
|
119
|
+
# objects: If we're given [:a, :b] we want the @deps hash to include
|
120
|
+
# both :a and :b as keys
|
121
|
+
@deps.values.each { |deps|
|
122
|
+
(deps.is_a?(Array) ? deps : [deps]).each { |d|
|
123
|
+
@deps[d] = [] if !@deps.key?(d)
|
124
|
+
}
|
125
|
+
}
|
126
|
+
end
|
127
|
+
|
128
|
+
# TSort virtual methods
|
129
|
+
def tsort_each_node(&block) @deps.each_key(&block) end
|
130
|
+
def tsort_each_child(node, &block) @deps[node].each(&block) end
|
131
|
+
|
132
|
+
private
|
133
|
+
def find_dependencies(a, &block)
|
134
|
+
block.call(a).each { |d|
|
135
|
+
(@deps[a] ||= []) << d
|
136
|
+
find_dependencies(d, &block)
|
137
|
+
}
|
138
|
+
end
|
139
|
+
end
|
140
|
+
end
|
141
|
+
end
|
142
|
+
|
143
|
+
|
144
|
+
|
145
|
+
|
146
|
+
|
147
|
+
|
148
|
+
|
149
|
+
|
150
|
+
|
151
|
+
|