prick 0.2.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.
- 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
|
+
|