prick 0.2.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,119 @@
1
+ module Prick
2
+ # Example:
3
+ #
4
+ # class Some
5
+ # include Ensure
6
+ #
7
+ # def exist?() ... end
8
+ # def create() ... end
9
+ # def destroy() ... end
10
+ #
11
+ # def loaded?() ... end
12
+ # def load() ... end
13
+ # def unload() ... end
14
+ #
15
+ # @ensure_states = {
16
+ # exist: [:create, :destroy],
17
+ # loaded: [:exist, :load, :unload] # Depends on :exist
18
+ # }
19
+ # end
20
+ #
21
+ # some = Some.new
22
+ # some.ensure_state(:loaded) # -> calls #create and then #load
23
+ # some.revoke_state(:loaded) # -> calls #unload but not #destroy
24
+ # some.ensure_state_value(:exist, false) # same as #revoke_state(:exist)
25
+ #
26
+ module Ensure
27
+ def ensure_state(state, *args) ensure_state_value(state, true, *args) end
28
+ def revoke_state(state, *args) ensure_state_value(state, false, *args) end
29
+ def ensure_state_value(state, value, *args) EnsureMethods.ensure(self, state, value, *args) end
30
+ end
31
+
32
+ # Helper module
33
+ module EnsureMethods
34
+ def self.klass(module_or_object)
35
+ module_or_object.is_a?(Module) ? module_or_object : module_or_object.class
36
+ end
37
+
38
+ def self.all_included(m)
39
+ m.singleton_class.included_modules
40
+ end
41
+
42
+ def self.all_extended(c)
43
+ c.ancestors
44
+ end
45
+
46
+ def self.all_states(c)
47
+ h = {}
48
+ (all_extended(c) + all_included(c)).reverse.each { |klass|
49
+ h.merge!(klass.instance_variable_get("@ensure_states") || {})
50
+ }
51
+ h
52
+ end
53
+
54
+ def self.ensure(module_or_object, state, expected, *args)
55
+ # module_or_object_text = module_or_object.is_a?(Module) ? "<module>" : "<object>"
56
+ # puts "ensure_state_impl(#{module_or_object_text.inspect}, #{state.inspect}, #{expected}, #{args.inspect})"
57
+
58
+ object = module_or_object
59
+ klass = self.klass(module_or_object)
60
+ value = call_method(object, :"#{state}?", *args)
61
+
62
+ if value != expected
63
+ entry = all_states(klass)[state] or
64
+ raise Prick::Error, "Can't find state #{state.inspect}"
65
+
66
+ # Handle ensure-pre-conditions recursively
67
+ precondition = (entry.size == 3 ? entry.shift : nil)
68
+ entry.size == 2 or raise "Malformed state entry for #{state.inspect}"
69
+ if precondition && expected # Don't tear down recursively
70
+ case precondition
71
+ when Symbol
72
+ self.ensure(object, precondition, true, *args)
73
+ when Proc
74
+ call_method(object, precondition, *args)
75
+ else
76
+ raise Prick::Fail, "Unexpected value: #{precondition.inspect}"
77
+ end
78
+ end
79
+
80
+ method = entry[expected ? 0 : 1]
81
+ case method
82
+ when true # a noop
83
+ ;
84
+ when false # an error
85
+ relation = (expected ? "to" : "from")
86
+ raise Error, "Can't change state #{relation} #{state.inspect}"
87
+ when Symbol # a method
88
+ call_method(object, method, *args)
89
+ when Proc # a lambda
90
+ call_method(object, method, *args)
91
+ else
92
+ raise Error, "Illegal @states entry: #{method.inspect}"
93
+ end
94
+ end
95
+ self # so that you can do 'some = Some.new.ensure_state(:loaded)' in one go
96
+ end
97
+
98
+ def self.vargs(arity, args)
99
+ n_args = (arity < 0 ? -(1 + arity) : arity)
100
+ args[0...n_args]
101
+ end
102
+
103
+ def self.call_method(object, symbol_or_lambda, *args)
104
+ # sol_text = symbol_or_lambda.is_a?(Proc) ? "<Proc>" : symbol_or_lambda
105
+ # puts "call_method(#{object.name}, #{sol_text.inspect}, #{args})"
106
+
107
+ if symbol_or_lambda.is_a?(Symbol)
108
+ executor = object.method(symbol_or_lambda)
109
+ executor.call(*vargs(executor.arity, args))
110
+ elsif symbol_or_lambda.is_a?(Proc)
111
+ executor = symbol_or_lambda
112
+ executor.call(object, *vargs(executor.arity, args))
113
+ else
114
+ raise Prick::Fail, "Illegal value: #{symbol_or_lambda.inspect}"
115
+ end
116
+ end
117
+ end
118
+ end
119
+
@@ -0,0 +1,13 @@
1
+
2
+ module Prick
3
+ class Error < RuntimeError; end
4
+ class Fail < Error; end
5
+ class NotYet < NotImplementedError
6
+ def initialize() super("Program error - Not yet implemented") end
7
+ end
8
+ class AbstractMethod < ScriptError
9
+ def initialize() super("Program error - Abstract method called") end
10
+ end
11
+ class Internal < ScriptError; end
12
+ end
13
+
@@ -0,0 +1,159 @@
1
+
2
+ require "prick/command.rb"
3
+
4
+ module Prick
5
+ # Tags have a 'v' prefixed the version in the git repository but this is made
6
+ # transparent to the application
7
+ module Git
8
+ def self.init
9
+ Command.command "git init"
10
+ end
11
+
12
+ # Returns true if the repository has no modified files. Requires the
13
+ # repository to have at least one commit. Creating the repository using
14
+ # Project::initialize_directory guarantees that
15
+ def self.clean?()
16
+ Command.command("git status").find { |l|
17
+ l =~ /Changes to be committed:/ || l =~ /^\s*modified:/
18
+ }.nil?
19
+ end
20
+
21
+ # Returns true if the repository is on a detached branch
22
+ def self.detached?
23
+ line = File.readlines(".git/HEAD").first.chomp
24
+ line =~ /^ref:/ ? false : true
25
+ end
26
+
27
+ # The current tag. This is only defined if the repository is in "detached
28
+ # head" mode
29
+ def self.current_tag() raise "Not yet implemented" end
30
+
31
+ # Return true if `version` has an associated tag
32
+ def self.tag?(version)
33
+ !list_tags.grep(version).empty?
34
+ end
35
+
36
+ # List tag versions
37
+ # Create version tag
38
+ def self.create_tag(version)
39
+ Command.command "git tag -a 'v#{version}' -m 'Release #{version}'"
40
+ end
41
+
42
+ def self.delete_tag(version, remote: false)
43
+ Command.command "git tag -d 'v#{version}'", fail: false
44
+ Command.command("git push --delete origin 'v#{version}'", fail: false) if remote
45
+ end
46
+
47
+ # Checkout a version tag as a detached head
48
+ def self.checkout_tag(version)
49
+ Command.command "git checkout 'v#{version}'"
50
+ end
51
+
52
+ def self.list_tags
53
+ Command.command("git tag").map { |tag| tag.sub(/^v(#{VERSION_SUB_RE})/, '\1') }
54
+ end
55
+
56
+ # Name of the current branch
57
+ def self.current_branch()
58
+ Command.command("git rev-parse --abbrev-ref HEAD").first
59
+ end
60
+
61
+ # Check if branch exist
62
+ def self.branch?(name)
63
+ Command.command("git show-ref --verify --quiet 'refs/heads/#{name}'", fail: false)
64
+ Command.status == 0
65
+ end
66
+
67
+ # Create a branch
68
+ def self.create_branch(name)
69
+ Command.command "git branch #{name}"
70
+ end
71
+
72
+ # Destroy branch
73
+ def self.delete_branch(name)
74
+ Command.command "git branch -D #{name}", fail: false
75
+ end
76
+
77
+ # Check out branch
78
+ def self.checkout_branch(name, create: false)
79
+ if create
80
+ Command.command "git checkout -b #{name}"
81
+ else
82
+ Command.command "git checkout #{name}"
83
+ end
84
+ end
85
+
86
+ # Merge a branch
87
+ def self.merge_branch(name, exclude_files: [], fail: false)
88
+ # Save content of excluded files
89
+ files = {}
90
+ exclude_files.each { |file|
91
+ next if !File.exist?(file)
92
+ files[file] = File.readlines(file)
93
+ }
94
+
95
+ Command.command "git merge --no-commit #{name}", fail: false
96
+
97
+ # Reinstate included files
98
+ files.each { |path, content|
99
+ File.open(path, "w") { |file| file.puts(content) }
100
+ # Resolve git unmerged status
101
+ Git.add(path)
102
+ }
103
+ end
104
+
105
+ # List branches
106
+ def self.list_branches
107
+ Command.command "git branch --format='%(refname:short)'"
108
+ end
109
+
110
+ # Add a file to the index of the current branch
111
+ def self.add(*files)
112
+ Array(files).each { |file|
113
+ Dir.chdir(File.dirname(file)) {
114
+ Command.command "git add '#{File.basename(file)}'"
115
+ }
116
+ }
117
+ end
118
+
119
+ # Return content of file in the given tag or branch. Defaults to HEAD
120
+ def self.readlines(file, tag: nil, branch: nil)
121
+ !(tag && branch) or raise Internal, "Can't use both tag: and branch: options"
122
+ if tag
123
+ Command.command "git show v#{tag}:#{file}"
124
+ else
125
+ branch ||= "HEAD"
126
+ Command.command "git show #{branch}:#{file}"
127
+ end.map { |l| "#{l}\n" }
128
+ end
129
+
130
+ # Return content of file
131
+ def self.read(file, tag: nil, branch: nil)
132
+ !(tag && branch) or raise Internal, "Can't use both tag: and branch: options"
133
+ if tag
134
+ Command.command "git show v#{tag}:#{file}"
135
+ else
136
+ branch ||= "HEAD"
137
+ Command.command "git show #{branch}:#{file}"
138
+ end.join("\n")
139
+ end
140
+
141
+ def self.rm(file)
142
+ Dir.chdir(File.dirname(file)) {
143
+ Command.command "git rm -f '#{File.basename(file)}'", fail: false
144
+ }
145
+ end
146
+
147
+ def self.rm_rf(file)
148
+ Dir.chdir(File.dirname(file)) {
149
+ Command.command "git rm -rf '#{File.basename(file)}'", fail: false
150
+ }
151
+ end
152
+
153
+ # Commit changes on the current branch"
154
+ def self.commit(msg)
155
+ Command.command "git commit -m '#{msg}'"
156
+ end
157
+ end
158
+ end
159
+
@@ -0,0 +1,22 @@
1
+
2
+ module Prick
3
+ module Migra
4
+ def self.migrate(from_name, to_name, file)
5
+ args = urls(from_name, to_name).join(" ")
6
+ options = OPTIONS.join(" ")
7
+ Command.command %(
8
+ status=0
9
+ migra #{options} #{args} >#{file} || { status=$?; }
10
+ [ $status = 2 ] || exit 1
11
+ )
12
+ end
13
+
14
+ private
15
+ OPTIONS = %w(--unsafe --with-privileges)
16
+
17
+ def self.urls(*args)
18
+ args = Array(args).flatten
19
+ args.map { |name| "postgresql:///#{name}" }
20
+ end
21
+ end
22
+ end
@@ -0,0 +1,230 @@
1
+
2
+ require 'yaml'
3
+
4
+ module Prick
5
+ class Migration
6
+ KEEP_FILE = ".keep"
7
+
8
+ attr_reader :path
9
+
10
+ def files() raise AbstractMethod end
11
+ def release_files() files end
12
+
13
+ # Return versions of the features in this migration
14
+ def feature_versions() raise AbstractMethod end
15
+
16
+ def initialize(path, template_dir)
17
+ @path = path
18
+ @template_dir = template_dir
19
+ end
20
+
21
+ def exist?() File.exist?(keep_file) end
22
+ def create() FileUtils.touch_p(keep_file); Git.add(keep_file) end
23
+ def destroy() Git.rm_rf(path) end
24
+
25
+ def present?() exist? && files.all? { |f| File.exist?(f) } end
26
+ def prepare() files.each { |f| FileUtils.cp(template_file(f), path) }; Git.add(path) end
27
+
28
+ def include_feature(feature) raise AbstractMethod end
29
+
30
+ def migrate() raise AbstractMethod end
31
+
32
+ def to_s() path end
33
+ def <=>(other) path <=> other.path end
34
+
35
+ def self.path(version)
36
+ if version.feature?
37
+ File.join(FEATURE_DIR, version.truncate(:feature).to_s, version.feature)
38
+ else
39
+ File.join(FEATURE_DIR, version)
40
+ end
41
+ end
42
+
43
+ def self.version(path)
44
+ Version.new(path.delete_prefix("#{FEATURE_DIR}/").sub("/", "_"))
45
+ end
46
+
47
+ def self.feature?(path)
48
+ Version.new(path.delete_prefix("#{FEATURE_DIR}/").sub("/", "_")).feature?
49
+ end
50
+
51
+ def self.files(path) raise AbstractMethod end
52
+ def self.release_files(path) files(path) end
53
+
54
+ def dump(&block)
55
+ # puts self.class
56
+ # indent {
57
+ # puts "path : #{path}"
58
+ # puts "files: #{files.inspect}"
59
+ # puts "release_files: #{release_files.inspect}"
60
+ # puts "feature_versions: #{feature_versions.map(&:to_s).inspect}"
61
+ # puts "keep_file: #{keep_file}"
62
+ # yield if block_given?
63
+ # }
64
+ end
65
+
66
+ protected
67
+ def template_file(path) File.join(SHARE_PATH, @template_dir, File.basename(path)) end
68
+ def keep_file() File.join(path, KEEP_FILE) end
69
+ end
70
+
71
+ class ReleaseMigration < Migration
72
+ FEATURES_TMPL_DIR = "features"
73
+ FILES = [
74
+ FEATURES_SQL = "features.sql",
75
+ FEATURES_YML = "features.yml",
76
+ MIGRATIONS_SQL = "migrations.sql",
77
+ DIFF_SQL = "diff.sql"
78
+ ]
79
+
80
+ attr_reader :features_yml
81
+ attr_reader :features_sql
82
+ attr_reader :migrations_sql
83
+ attr_reader :diff_sql
84
+
85
+ def files() [features_yml, features_sql, migrations_sql, diff_sql] end
86
+
87
+ def initialize(path)
88
+ super(path, FEATURES_TMPL_DIR)
89
+ @features_yml = File.join(path, FEATURES_YML)
90
+ @features_sql = File.join(path, FEATURES_SQL)
91
+ @migrations_sql = File.join(path, MIGRATIONS_SQL)
92
+ @diff_sql = File.join(path, DIFF_SQL)
93
+ end
94
+
95
+ def feature_versions() read_features_yml.map { |path| Migration.version(path) } end
96
+ def feature_paths() read_features_yml end
97
+
98
+ # `feature` is a Feature object
99
+ def include_feature(migration, append: true)
100
+ migration.is_a?(FeatureMigration) or raise "Expected FeatureMigration object, got #{migration.class}"
101
+ !feature_paths.include?(migration.path) or raise Error, "Feature #{migration.version} is already included"
102
+ exclude_files = [Schema.data_file] +
103
+ migration.release_files +
104
+ migration.feature_paths.map { |path| FeatureMigration.release_files(path) }.flatten
105
+
106
+ Git.merge_branch(migration.version, exclude_files: exclude_files)
107
+
108
+ feature_paths = YAML.load(Git.read(migration.features_yml, branch: migration.version))
109
+ append_features_yml(feature_paths, append: append)
110
+
111
+ feature_paths.each { |feature_path|
112
+ if path != File.dirname(feature_path)
113
+ FileUtils.ln_s(File.join("../..", feature_path), path)
114
+ Git.add(File.join(path, File.basename(feature_path)))
115
+ end
116
+ }
117
+ end
118
+
119
+ def migrate(database_name)
120
+ puts "ReleaseMigration#migrate"
121
+ if File.exist?(migrations_sql)
122
+ Dir.chdir(path) {
123
+ puts " cd #{path}"
124
+ puts " psql -d #{database_name} < #{MIGRATIONS_SQL}"
125
+ Command.command "psql -d #{database_name} < #{MIGRATIONS_SQL}"
126
+ }
127
+ end
128
+ end
129
+
130
+
131
+ def self.files(path)
132
+ FILES.map { |file| File.join(path, file) }
133
+ end
134
+
135
+ def read_features_yml(branch = nil) YAML.load(File.read(features_yml)) || [] end
136
+
137
+ def append_features_yml(paths, append: true)
138
+ write_features_yml(read_features_yml.insert(append ? -1 : 0, *paths))
139
+ end
140
+
141
+ def write_features_yml(paths)
142
+ FileUtils.cp(template_file(features_yml), features_yml)
143
+ File.open(features_yml, "a") { |f| f.write(paths.to_yaml) }
144
+ FileUtils.cp(template_file(features_sql), features_sql)
145
+ File.open(features_sql, "a") { |f|
146
+ paths.map { |path|
147
+ f.puts "\\cd #{File.basename(path)}"
148
+ f.puts "\\i migrate.sql" }
149
+ f.puts "\\cd .."
150
+ f.puts
151
+ }
152
+ Git.add(features_yml)
153
+ Git.add(features_sql)
154
+ end
155
+ end
156
+
157
+ class FeatureMigration < Migration
158
+ FEATURE_TMPL_DIR = "features/feature"
159
+ FILES = [
160
+ MIGRATE_SQL = "migrate.sql",
161
+ DIFF_SQL = "diff.sql"
162
+ ]
163
+
164
+ attr_reader :name
165
+ attr_reader :version
166
+ attr_reader :release_migration # The enclosing release migration
167
+
168
+ attr_reader :migrate_sql
169
+ attr_reader :diff_sql
170
+
171
+ def features_yml() release_migration.features_yml end
172
+ def features_sql() release_migration.features_sql end
173
+ def migrations_sql() release_migration.migrations_sql end
174
+ def release_diff_sql() release_migration.diff_sql end
175
+
176
+ def files() [migrate_sql, diff_sql] end
177
+ def release_files() release_migration.files end
178
+
179
+ def feature_versions() release_migration.feature_versions end
180
+ def feature_paths() release_migration.feature_paths end
181
+
182
+ def initialize(path, name: File.basename(path))
183
+ Migration.feature?(path) or raise "Expected a feature path, got #{path}"
184
+ super(path, FEATURE_TMPL_DIR)
185
+ @name = name
186
+ @version = Migration.version(path)
187
+ @release_migration = ReleaseMigration.new(File.dirname(path))
188
+ @migrate_sql = File.join(path, MIGRATE_SQL)
189
+ @diff_sql = File.join(path, DIFF_SQL)
190
+ end
191
+
192
+ def exist?() release_migration.exist? && super end
193
+ def create()
194
+ release_migration.exist? || release_migration.create
195
+ super
196
+ end
197
+
198
+ def present?() release_migration.present? && super end
199
+ def prepare()
200
+ release_migration.present? || release_migration.prepare
201
+ super
202
+ release_migration.append_features_yml([path])
203
+ end
204
+
205
+ def include_feature(migration)
206
+ release_migration.include_feature(migration, append: false)
207
+ end
208
+
209
+ def self.files(path)
210
+ release_files + FILES.map { |file| File.join(path, file) }
211
+ end
212
+
213
+ def self.release_files(path)
214
+ ReleaseMigration.files(path)
215
+ end
216
+
217
+ def dump
218
+ super {
219
+ puts "name: #{name}"
220
+ puts "release_migration: #{path}"
221
+ }
222
+ end
223
+ end
224
+ end
225
+
226
+
227
+
228
+
229
+
230
+