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.
@@ -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
+