penman 0.4.9
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +7 -0
- data/MIT-LICENSE +20 -0
- data/Rakefile +19 -0
- data/config/routes.rb +2 -0
- data/db/migrate/20150909222413_create_record_tags.rb +14 -0
- data/lib/penman/configuration.rb +36 -0
- data/lib/penman/engine.rb +11 -0
- data/lib/penman/penman_exceptions.rb +8 -0
- data/lib/penman/record_tag.rb +330 -0
- data/lib/penman/seed_code.rb +21 -0
- data/lib/penman/seed_file_generator.rb +26 -0
- data/lib/penman/taggable.rb +11 -0
- data/lib/penman/version.rb +7 -0
- data/lib/penman.rb +39 -0
- data/lib/tasks/penman_tasks.rake +6 -0
- data/lib/templates/default.rb.erb +7 -0
- metadata +154 -0
checksums.yaml
ADDED
@@ -0,0 +1,7 @@
|
|
1
|
+
---
|
2
|
+
SHA1:
|
3
|
+
metadata.gz: 2f3f532cb311801e2259b8040160fa9f090ee507
|
4
|
+
data.tar.gz: 2f7920834834e4e048decc6d57b53e8fcb408725
|
5
|
+
SHA512:
|
6
|
+
metadata.gz: 83bf9f99fe187dc2c74c5cff9a3447df3abb8dfee865ad0d08bb016de6b3d5b1ce33a1296c0cf6eabe7cda6f89c82e13f88d436ddca94d4c3e6b4a8c9ed76981
|
7
|
+
data.tar.gz: 3ecb902578d20e1a541d5fea6d3fdaac49026c8fa1a359426a3dda12dfede8180a887b9d83e2621004a5cf954ded22aa741754a1a0eef40411052343575e8ad6
|
data/MIT-LICENSE
ADDED
@@ -0,0 +1,20 @@
|
|
1
|
+
Copyright 2015 YOURNAME
|
2
|
+
|
3
|
+
Permission is hereby granted, free of charge, to any person obtaining
|
4
|
+
a copy of this software and associated documentation files (the
|
5
|
+
"Software"), to deal in the Software without restriction, including
|
6
|
+
without limitation the rights to use, copy, modify, merge, publish,
|
7
|
+
distribute, sublicense, and/or sell copies of the Software, and to
|
8
|
+
permit persons to whom the Software is furnished to do so, subject to
|
9
|
+
the following conditions:
|
10
|
+
|
11
|
+
The above copyright notice and this permission notice shall be
|
12
|
+
included in all copies or substantial portions of the Software.
|
13
|
+
|
14
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
15
|
+
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
|
16
|
+
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
|
17
|
+
NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
|
18
|
+
LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
|
19
|
+
OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
|
20
|
+
WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
data/Rakefile
ADDED
@@ -0,0 +1,19 @@
|
|
1
|
+
begin
|
2
|
+
require 'bundler/setup'
|
3
|
+
rescue LoadError
|
4
|
+
puts 'You must `gem install bundler` and `bundle install` to run rake tasks'
|
5
|
+
end
|
6
|
+
|
7
|
+
require 'rdoc/task'
|
8
|
+
|
9
|
+
RDoc::Task.new(:rdoc) do |rdoc|
|
10
|
+
rdoc.rdoc_dir = 'rdoc'
|
11
|
+
rdoc.title = 'Penman'
|
12
|
+
rdoc.options << '--line-numbers'
|
13
|
+
rdoc.rdoc_files.include('README.rdoc')
|
14
|
+
rdoc.rdoc_files.include('lib/**/*.rb')
|
15
|
+
end
|
16
|
+
|
17
|
+
Bundler::GemHelper.install_tasks
|
18
|
+
|
19
|
+
Dir["#{File.dirname(__FILE__)}/lib/tasks/*.rake"].sort.each { |ext| load ext }
|
data/config/routes.rb
ADDED
@@ -0,0 +1,14 @@
|
|
1
|
+
class CreateRecordTags < ActiveRecord::Migration
|
2
|
+
def change
|
3
|
+
create_table :record_tags do |t|
|
4
|
+
t.integer :record_id, null: false, default: 0
|
5
|
+
t.string :record_type, null: false
|
6
|
+
t.string :candidate_key, null: false
|
7
|
+
t.string :tag, null: false
|
8
|
+
t.boolean :created_this_session, null: false, default: false
|
9
|
+
t.timestamps
|
10
|
+
end
|
11
|
+
|
12
|
+
add_index 'record_tags', ['record_id', 'record_type'], name: 'index_record_tags_on_record_id_and_record_type', using: :btree
|
13
|
+
end
|
14
|
+
end
|
@@ -0,0 +1,36 @@
|
|
1
|
+
module Penman
|
2
|
+
class Configuration
|
3
|
+
attr_accessor :seed_path,
|
4
|
+
:default_candidate_key,
|
5
|
+
:seed_template_file,
|
6
|
+
:file_name_formatter,
|
7
|
+
:after_generate,
|
8
|
+
:validate_records_before_seed_generation
|
9
|
+
|
10
|
+
def initialize
|
11
|
+
@seed_path = 'db/migrate'
|
12
|
+
@default_candidate_key = :reference
|
13
|
+
|
14
|
+
root = File.expand_path '../..', __FILE__
|
15
|
+
@seed_template_file = File.join(root, 'templates', 'default.rb.erb')
|
16
|
+
|
17
|
+
@file_name_formatter = lambda do |model_name, seed_type|
|
18
|
+
"#{model_name.underscore.pluralize}_#{seed_type}"
|
19
|
+
end
|
20
|
+
|
21
|
+
@after_generate = lambda do |version, name|
|
22
|
+
return unless ActiveRecord::Base.connection.table_exists? 'schema_migrations'
|
23
|
+
|
24
|
+
unless Object.const_defined?('SchemaMigration')
|
25
|
+
Object.const_set('SchemaMigration', Class.new(ActiveRecord::Base))
|
26
|
+
end
|
27
|
+
|
28
|
+
return unless SchemaMigration.column_names.include? 'version'
|
29
|
+
|
30
|
+
SchemaMigration.find_or_create_by(version: version)
|
31
|
+
end
|
32
|
+
|
33
|
+
@validate_records_before_seed_generation = false
|
34
|
+
end
|
35
|
+
end
|
36
|
+
end
|
@@ -0,0 +1,11 @@
|
|
1
|
+
module Penman
|
2
|
+
class Engine < ::Rails::Engine
|
3
|
+
initializer :append_migrations do |app|
|
4
|
+
unless app.config.paths["db/migrate"].include? root.to_s
|
5
|
+
config.paths["db/migrate"].expanded.each do |expanded_path|
|
6
|
+
app.config.paths["db/migrate"] << expanded_path
|
7
|
+
end
|
8
|
+
end
|
9
|
+
end
|
10
|
+
end
|
11
|
+
end
|
@@ -0,0 +1,8 @@
|
|
1
|
+
module RecordTagExceptions
|
2
|
+
RecordTagError = Class.new(StandardError)
|
3
|
+
|
4
|
+
InvalidCandidateKeyForRecord = Class.new(RecordTagError)
|
5
|
+
RecordNotFound = Class.new(RecordTagError)
|
6
|
+
TooManyTagsForRecord = Class.new(RecordTagError)
|
7
|
+
BadTracking = Class.new(RecordTagError)
|
8
|
+
end
|
@@ -0,0 +1,330 @@
|
|
1
|
+
require 'active_record'
|
2
|
+
require 'penman/penman_exceptions'
|
3
|
+
require 'penman/seed_file_generator'
|
4
|
+
require 'penman/seed_code'
|
5
|
+
|
6
|
+
module Penman
|
7
|
+
class RecordTag < ActiveRecord::Base
|
8
|
+
belongs_to :record, polymorphic: true
|
9
|
+
validates_uniqueness_of :tag, scope: [:record_type, :record_id]
|
10
|
+
|
11
|
+
before_save :encode_candidate_key
|
12
|
+
|
13
|
+
def encode_candidate_key
|
14
|
+
if self.candidate_key.is_a? Hash
|
15
|
+
self.candidate_key = self.candidate_key.to_json
|
16
|
+
end
|
17
|
+
end
|
18
|
+
|
19
|
+
def decode_candidate_key(key)
|
20
|
+
begin
|
21
|
+
ActiveSupport::JSON.decode(key).symbolize_keys
|
22
|
+
rescue JSON::ParserError
|
23
|
+
# This will occur if the candidate key isn't encoded as json.
|
24
|
+
# An example of this would be when we are tagging yaml files as touched when messing with lang.
|
25
|
+
# In that case we store the file path in the candidate key column as a regular string.
|
26
|
+
key
|
27
|
+
end
|
28
|
+
end
|
29
|
+
|
30
|
+
@@enabled = false
|
31
|
+
@@taggable_models = []
|
32
|
+
|
33
|
+
def candidate_key
|
34
|
+
decode_candidate_key(super)
|
35
|
+
end
|
36
|
+
|
37
|
+
class << self
|
38
|
+
def disable
|
39
|
+
@@enabled = false
|
40
|
+
end
|
41
|
+
|
42
|
+
def enable
|
43
|
+
@@enabled = true
|
44
|
+
end
|
45
|
+
|
46
|
+
def enabled?
|
47
|
+
@@enabled
|
48
|
+
end
|
49
|
+
|
50
|
+
def register(model)
|
51
|
+
@@taggable_models |= [model]
|
52
|
+
end
|
53
|
+
|
54
|
+
def tag(record, tag)
|
55
|
+
return unless @@enabled
|
56
|
+
candidate_key = record.class.try(:candidate_key) || Penman.config.default_candidate_key
|
57
|
+
candidate_key = [candidate_key] unless candidate_key.is_a? Array
|
58
|
+
raise RecordTagExceptions::InvalidCandidateKeyForRecord unless record_has_attributes?(record, candidate_key)
|
59
|
+
|
60
|
+
candidate_key_to_store =
|
61
|
+
if ['created', 'destroyed'].include? tag
|
62
|
+
Hash[candidate_key.map { |k| [k, record.send(k)] }].to_json
|
63
|
+
else # updated
|
64
|
+
Hash[candidate_key.map { |k| [k, record.send("#{k}_was")] }].to_json
|
65
|
+
end
|
66
|
+
|
67
|
+
created_tag = RecordTag.find_by(record: record, tag: 'created')
|
68
|
+
updated_tag = RecordTag.find_by(record: record, tag: 'updated')
|
69
|
+
destroyed_tag = RecordTag.find_by(record_type: record.class.name, candidate_key: candidate_key_to_store, tag: 'destroyed')
|
70
|
+
|
71
|
+
raise RecordTagExceptions::TooManyTagsForRecord if [created_tag, updated_tag, destroyed_tag].count { |t| t.present? } > 1
|
72
|
+
|
73
|
+
if created_tag.present?
|
74
|
+
case tag
|
75
|
+
when 'created'
|
76
|
+
raise RecordTagExceptions::BadTracking, format_error_message('created', 'created', candidate_key_to_store)
|
77
|
+
when 'updated'
|
78
|
+
created_tag.update!(tag: tag)
|
79
|
+
when 'destroyed'
|
80
|
+
created_tag.destroy!
|
81
|
+
end
|
82
|
+
elsif updated_tag.present?
|
83
|
+
case tag
|
84
|
+
when 'created'
|
85
|
+
raise RecordTagExceptions::BadTracking, format_error_message('updated', 'created', candidate_key_to_store)
|
86
|
+
when 'updated'
|
87
|
+
updated_tag.update!(tag: tag)
|
88
|
+
when 'destroyed'
|
89
|
+
if updated_tag.created_this_session
|
90
|
+
updated_tag.destroy!
|
91
|
+
else
|
92
|
+
updated_tag.update!(tag: tag)
|
93
|
+
end
|
94
|
+
end
|
95
|
+
elsif destroyed_tag.present?
|
96
|
+
case tag
|
97
|
+
when 'created'
|
98
|
+
# We make an updated tag in case non-candidate key attributes have changed, since we don't tack those.
|
99
|
+
destroyed_tag.update!(tag: 'updated', record_id: record.id)
|
100
|
+
when 'updated'
|
101
|
+
raise RecordTagExceptions::BadTracking, format_error_message('destroyed', 'updated', candidate_key_to_store)
|
102
|
+
when 'destroyed'
|
103
|
+
raise RecordTagExceptions::BadTracking, format_error_message('destroyed', 'destroyed', candidate_key_to_store)
|
104
|
+
end
|
105
|
+
else # new tag
|
106
|
+
RecordTag.create!(record: record, tag: tag, candidate_key: candidate_key_to_store, created_this_session: tag == 'created')
|
107
|
+
end
|
108
|
+
end
|
109
|
+
|
110
|
+
def find_tags_for_model(model)
|
111
|
+
find_tags_for_models(model)
|
112
|
+
end
|
113
|
+
|
114
|
+
def find_tags_for_models(*models)
|
115
|
+
RecordTag.where(record_type: models.map { |m| (m.is_a? String) ? m : m.name })
|
116
|
+
end
|
117
|
+
|
118
|
+
def create_custom(attributes = {})
|
119
|
+
attributes = { record_type: 'custom_tag', tag: 'touched', candidate_key: 'n/a' }.merge attributes
|
120
|
+
record_tag = RecordTag.find_or_create_by(attributes)
|
121
|
+
record_tag.update(record_id: record_tag.id) if record_tag.record_id == 0 # notice validation above, this just ensures that we don't violate the table constraint.
|
122
|
+
end
|
123
|
+
|
124
|
+
def generate_seeds
|
125
|
+
generate_seed_for_models(seed_order)
|
126
|
+
end
|
127
|
+
|
128
|
+
def generate_seed_for_models(models)
|
129
|
+
time = Time.now
|
130
|
+
seed_files = []
|
131
|
+
|
132
|
+
models.each do |model|
|
133
|
+
seed_files << generate_update_seed(model, time.strftime('%Y%m%d%H%M%S'))
|
134
|
+
time += 1.second
|
135
|
+
end
|
136
|
+
|
137
|
+
models.reverse.each do |model|
|
138
|
+
seed_files << generate_destroy_seed(model, time.strftime('%Y%m%d%H%M%S'))
|
139
|
+
time += 1.second
|
140
|
+
end
|
141
|
+
|
142
|
+
RecordTag.where(record_type: models.map(&:name)).destroy_all
|
143
|
+
seed_files.compact
|
144
|
+
end
|
145
|
+
|
146
|
+
def generate_seed_for_model(model)
|
147
|
+
time = Time.now
|
148
|
+
seed_files = []
|
149
|
+
seed_files << generate_update_seed(model, time.strftime('%Y%m%d%H%M%S'))
|
150
|
+
seed_files << generate_destroy_seed(model, (time + 1.second).strftime('%Y%m%d%H%M%S'))
|
151
|
+
RecordTag.where(record_type: model.name).delete_all
|
152
|
+
seed_files.compact
|
153
|
+
end
|
154
|
+
|
155
|
+
private
|
156
|
+
def reset_tree
|
157
|
+
@@roots = []
|
158
|
+
@@tree = {}
|
159
|
+
@@polymorphic = []
|
160
|
+
end
|
161
|
+
|
162
|
+
def add_model_to_tree(model)
|
163
|
+
reflections = model.reflect_on_all_associations(:belongs_to)
|
164
|
+
|
165
|
+
if reflections.find { |r| r.options[:polymorphic] }.present?
|
166
|
+
@@polymorphic << model
|
167
|
+
else
|
168
|
+
@@roots.push(model) unless @@tree.key?(model)
|
169
|
+
end
|
170
|
+
|
171
|
+
@@tree[model] = reflections.reject { |r| r.options[:polymorphic] || r.klass == model }.map(&:klass)
|
172
|
+
@@tree[model].each { |ch| @@tree[ch] ||= [] }
|
173
|
+
|
174
|
+
@@roots -= @@tree[model]
|
175
|
+
end
|
176
|
+
|
177
|
+
def seed_order
|
178
|
+
reset_tree
|
179
|
+
@@taggable_models.each { |m| add_model_to_tree(m) }
|
180
|
+
|
181
|
+
seed_order = []
|
182
|
+
|
183
|
+
recurse_on = -> (node) do
|
184
|
+
return unless node.ancestors.include?(Taggable)
|
185
|
+
@@tree[node].each { |n| recurse_on.call(n) }
|
186
|
+
seed_order |= [node]
|
187
|
+
end
|
188
|
+
|
189
|
+
@@roots.each { |node| recurse_on.call(node) }
|
190
|
+
@@polymorphic.each { |node| recurse_on.call(node) }
|
191
|
+
|
192
|
+
seed_order | @@polymorphic
|
193
|
+
end
|
194
|
+
|
195
|
+
def generate_update_seed(model, timestamp)
|
196
|
+
validate_records_for_model(model) if Penman.config.validate_records_before_seed_generation
|
197
|
+
touched_tags = RecordTag.where(record_type: model.name, tag: ['created', 'updated']).includes(:record)
|
198
|
+
return nil if touched_tags.empty?
|
199
|
+
seed_code = SeedCode.new
|
200
|
+
seed_code << 'penman_initially_enabled = Penman.enabled?'
|
201
|
+
seed_code << 'Penman.disable'
|
202
|
+
|
203
|
+
touched_tags.each do |tag|
|
204
|
+
seed_code << "# Generating seed for #{tag.tag.upcase} tag."
|
205
|
+
seed_code << "record = #{model.name}.find_by(#{print_candidate_key(tag.record)})"
|
206
|
+
seed_code << "record = #{model.name}.find_or_initialize_by(#{attribute_string_from_hash(model, tag.candidate_key)}) if record.nil?"
|
207
|
+
|
208
|
+
column_hash = Hash[
|
209
|
+
model.attribute_names
|
210
|
+
.reject { |col| col == model.primary_key }
|
211
|
+
.map { |col| [col, tag.record.send(col)] }
|
212
|
+
]
|
213
|
+
|
214
|
+
seed_code << "record.update!(#{attribute_string_from_hash(model, column_hash)})"
|
215
|
+
end
|
216
|
+
|
217
|
+
seed_code << 'Penman.enable if penman_initially_enabled'
|
218
|
+
seed_file_name = Penman.config.file_name_formatter.call(model.name, 'updates')
|
219
|
+
sfg = SeedFileGenerator.new(seed_file_name, timestamp, seed_code)
|
220
|
+
sfg.write_seed
|
221
|
+
end
|
222
|
+
|
223
|
+
def generate_destroy_seed(model, timestamp)
|
224
|
+
destroyed_tags = RecordTag.where(record_type: model.name, tag: 'destroyed')
|
225
|
+
return nil if destroyed_tags.empty?
|
226
|
+
seed_code = SeedCode.new
|
227
|
+
seed_code << 'penman_initially_enabled = Penman.enabled?'
|
228
|
+
seed_code << 'Penman.disable'
|
229
|
+
|
230
|
+
destroyed_tags.map(&:candidate_key).each do |record_candidate_key|
|
231
|
+
seed_code << "record = #{model.name}.find_by(#{attribute_string_from_hash(model, record_candidate_key)})"
|
232
|
+
seed_code << "record.try(:destroy)"
|
233
|
+
end
|
234
|
+
|
235
|
+
seed_code << 'Penman.enable if penman_initially_enabled'
|
236
|
+
seed_file_name = Penman.config.file_name_formatter.call(model.name, 'destroys')
|
237
|
+
sfg = SeedFileGenerator.new(seed_file_name, timestamp, seed_code)
|
238
|
+
sfg.write_seed
|
239
|
+
end
|
240
|
+
|
241
|
+
def validate_records_for_model(model)
|
242
|
+
RecordTag.where(record_type: model.name, tag: ['updated', 'created'])
|
243
|
+
.includes(:record)
|
244
|
+
.each { |r| r.record.validate! }
|
245
|
+
end
|
246
|
+
|
247
|
+
def print_candidate_key(record)
|
248
|
+
candidate_key = record.class.try(:candidate_key) || Penman.config.default_candidate_key
|
249
|
+
candidate_key = [candidate_key] unless candidate_key.is_a? Array
|
250
|
+
raise RecordTagExceptions::InvalidCandidateKeyForRecord unless record_has_attributes?(record, candidate_key)
|
251
|
+
|
252
|
+
candidate_key_hash = {}
|
253
|
+
candidate_key.each { |key| candidate_key_hash[key] = record.send(key) }
|
254
|
+
attribute_string_from_hash(record.class, candidate_key_hash)
|
255
|
+
end
|
256
|
+
|
257
|
+
def attribute_string_from_hash(model, column_hash)
|
258
|
+
column_hash.symbolize_keys!
|
259
|
+
formatted_candidate_key = []
|
260
|
+
|
261
|
+
column_hash.each do |k, v|
|
262
|
+
reflection = find_foreign_key_relation(model, k)
|
263
|
+
|
264
|
+
if reflection && v.present?
|
265
|
+
associated_model = if reflection.polymorphic?
|
266
|
+
column_hash[reflection.foreign_type.to_sym].constantize
|
267
|
+
else
|
268
|
+
reflection.klass
|
269
|
+
end
|
270
|
+
|
271
|
+
if associated_model.ancestors.include?(Taggable) || associated_model.respond_to?(:candidate_key)
|
272
|
+
primary_key = reflection.options[:primary_key] || associated_model.primary_key
|
273
|
+
associated_record = associated_model.find_by(primary_key => v)
|
274
|
+
to_add = "#{reflection.name}: #{associated_model.name}.find_by("
|
275
|
+
|
276
|
+
if associated_record.present?
|
277
|
+
to_add += "#{print_candidate_key(associated_record)})"
|
278
|
+
else # likely this record was destroyed, so we should have a tag for it
|
279
|
+
tag = RecordTag.find_by(record_type: associated_model.name, record_id: v)
|
280
|
+
raise RecordTagExceptions::RecordNotFound, "while processing #{column_hash}" if tag.nil?
|
281
|
+
to_add += "#{attribute_string_from_hash(associated_model, tag.candidate_key)})"
|
282
|
+
end
|
283
|
+
|
284
|
+
formatted_candidate_key << to_add
|
285
|
+
next
|
286
|
+
end
|
287
|
+
end
|
288
|
+
|
289
|
+
formatted_candidate_key << "#{k}: #{primitive_string(v)}"
|
290
|
+
end
|
291
|
+
|
292
|
+
formatted_candidate_key.join(', ')
|
293
|
+
end
|
294
|
+
|
295
|
+
def find_foreign_key_relation(model, accessor)
|
296
|
+
model.reflect_on_all_associations.find do |r|
|
297
|
+
begin
|
298
|
+
r.foreign_key.to_sym == accessor.to_sym
|
299
|
+
rescue NameError
|
300
|
+
false
|
301
|
+
end
|
302
|
+
end
|
303
|
+
end
|
304
|
+
|
305
|
+
def record_has_attributes?(record, attributes)
|
306
|
+
attributes.each do |attribute|
|
307
|
+
return false unless record.has_attribute?(attribute)
|
308
|
+
end
|
309
|
+
|
310
|
+
true
|
311
|
+
end
|
312
|
+
|
313
|
+
def primitive_string(p)
|
314
|
+
if p.nil?
|
315
|
+
'nil'
|
316
|
+
elsif p.is_a? String
|
317
|
+
"'#{p}'"
|
318
|
+
elsif p.is_a? Time
|
319
|
+
"Time.parse('#{p}')"
|
320
|
+
else
|
321
|
+
"#{p}"
|
322
|
+
end
|
323
|
+
end
|
324
|
+
|
325
|
+
def format_error_message(existing_tag, new_tag, record_to_store)
|
326
|
+
"found an existing '#{existing_tag}' tag for record while tagging, '#{new_tag}' - #{record_to_store}"
|
327
|
+
end
|
328
|
+
end
|
329
|
+
end
|
330
|
+
end
|
@@ -0,0 +1,21 @@
|
|
1
|
+
module Penman
|
2
|
+
class SeedCode
|
3
|
+
def initialize(seed_code = [])
|
4
|
+
@seed_code = seed_code
|
5
|
+
end
|
6
|
+
|
7
|
+
def << (seed_line)
|
8
|
+
@seed_code << seed_line
|
9
|
+
end
|
10
|
+
|
11
|
+
def print_with_leading_spaces(num_spaces)
|
12
|
+
spaces = "\n" + ' ' * num_spaces
|
13
|
+
@seed_code.join(spaces)
|
14
|
+
end
|
15
|
+
|
16
|
+
def print_with_leading_tabs(num_tabs)
|
17
|
+
tabs = "\n" + "\t" * num_tabs
|
18
|
+
@seed_code.join(tabs)
|
19
|
+
end
|
20
|
+
end
|
21
|
+
end
|
@@ -0,0 +1,26 @@
|
|
1
|
+
module Penman
|
2
|
+
class SeedFileGenerator
|
3
|
+
attr_reader :seed_code
|
4
|
+
attr_reader :file_name
|
5
|
+
attr_reader :timestamp
|
6
|
+
|
7
|
+
def initialize(file_name, timestamp, seed_code)
|
8
|
+
@seed_code = seed_code
|
9
|
+
@file_name = file_name
|
10
|
+
@timestamp = timestamp
|
11
|
+
end
|
12
|
+
|
13
|
+
def write_seed
|
14
|
+
erb = ERB.new(File.read(Penman.config.seed_template_file))
|
15
|
+
seed_file_name = "#{@timestamp}_#{@file_name}.rb"
|
16
|
+
full_seed_file_path = File.join(Penman.config.seed_path, seed_file_name)
|
17
|
+
IO.write(full_seed_file_path, erb.result(binding))
|
18
|
+
|
19
|
+
if Penman.config.after_generate.present?
|
20
|
+
Penman.config.after_generate.call(@timestamp, @file_name)
|
21
|
+
end
|
22
|
+
|
23
|
+
full_seed_file_path
|
24
|
+
end
|
25
|
+
end
|
26
|
+
end
|
@@ -0,0 +1,11 @@
|
|
1
|
+
module Taggable
|
2
|
+
extend ActiveSupport::Concern
|
3
|
+
|
4
|
+
included do
|
5
|
+
after_create { Penman::RecordTag.tag(self, 'created') }
|
6
|
+
after_update { Penman::RecordTag.tag(self, 'updated') }
|
7
|
+
after_destroy { Penman::RecordTag.tag(self, 'destroyed') }
|
8
|
+
|
9
|
+
Penman::RecordTag.register(self)
|
10
|
+
end
|
11
|
+
end
|
data/lib/penman.rb
ADDED
@@ -0,0 +1,39 @@
|
|
1
|
+
require "penman/engine"
|
2
|
+
require 'penman/configuration'
|
3
|
+
require 'penman/record_tag'
|
4
|
+
require 'penman/taggable'
|
5
|
+
require 'penman/seed_file_generator'
|
6
|
+
|
7
|
+
module Penman
|
8
|
+
class << self
|
9
|
+
attr_writer :config
|
10
|
+
end
|
11
|
+
|
12
|
+
def self.config
|
13
|
+
@config ||= Configuration.new
|
14
|
+
end
|
15
|
+
|
16
|
+
def self.configure
|
17
|
+
yield(config)
|
18
|
+
end
|
19
|
+
|
20
|
+
def self.reset
|
21
|
+
@config = Configuration.new
|
22
|
+
end
|
23
|
+
|
24
|
+
def self.enable
|
25
|
+
RecordTag.enable
|
26
|
+
end
|
27
|
+
|
28
|
+
def self.disable
|
29
|
+
RecordTag.disable
|
30
|
+
end
|
31
|
+
|
32
|
+
def self.enabled?
|
33
|
+
RecordTag.enabled?
|
34
|
+
end
|
35
|
+
|
36
|
+
def self.generate_seeds
|
37
|
+
RecordTag.generate_seeds
|
38
|
+
end
|
39
|
+
end
|
metadata
ADDED
@@ -0,0 +1,154 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: penman
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
version: 0.4.9
|
5
|
+
platform: ruby
|
6
|
+
authors:
|
7
|
+
- Mat Pataki
|
8
|
+
autorequire:
|
9
|
+
bindir: bin
|
10
|
+
cert_chain: []
|
11
|
+
date: 2016-03-07 00:00:00.000000000 Z
|
12
|
+
dependencies:
|
13
|
+
- !ruby/object:Gem::Dependency
|
14
|
+
name: rails
|
15
|
+
requirement: !ruby/object:Gem::Requirement
|
16
|
+
requirements:
|
17
|
+
- - "~>"
|
18
|
+
- !ruby/object:Gem::Version
|
19
|
+
version: '4'
|
20
|
+
type: :runtime
|
21
|
+
prerelease: false
|
22
|
+
version_requirements: !ruby/object:Gem::Requirement
|
23
|
+
requirements:
|
24
|
+
- - "~>"
|
25
|
+
- !ruby/object:Gem::Version
|
26
|
+
version: '4'
|
27
|
+
- !ruby/object:Gem::Dependency
|
28
|
+
name: mysql2
|
29
|
+
requirement: !ruby/object:Gem::Requirement
|
30
|
+
requirements:
|
31
|
+
- - "~>"
|
32
|
+
- !ruby/object:Gem::Version
|
33
|
+
version: '0.3'
|
34
|
+
- - ">="
|
35
|
+
- !ruby/object:Gem::Version
|
36
|
+
version: 0.3.18
|
37
|
+
type: :development
|
38
|
+
prerelease: false
|
39
|
+
version_requirements: !ruby/object:Gem::Requirement
|
40
|
+
requirements:
|
41
|
+
- - "~>"
|
42
|
+
- !ruby/object:Gem::Version
|
43
|
+
version: '0.3'
|
44
|
+
- - ">="
|
45
|
+
- !ruby/object:Gem::Version
|
46
|
+
version: 0.3.18
|
47
|
+
- !ruby/object:Gem::Dependency
|
48
|
+
name: rspec-rails
|
49
|
+
requirement: !ruby/object:Gem::Requirement
|
50
|
+
requirements:
|
51
|
+
- - "~>"
|
52
|
+
- !ruby/object:Gem::Version
|
53
|
+
version: '3.3'
|
54
|
+
- - ">="
|
55
|
+
- !ruby/object:Gem::Version
|
56
|
+
version: 3.3.3
|
57
|
+
type: :development
|
58
|
+
prerelease: false
|
59
|
+
version_requirements: !ruby/object:Gem::Requirement
|
60
|
+
requirements:
|
61
|
+
- - "~>"
|
62
|
+
- !ruby/object:Gem::Version
|
63
|
+
version: '3.3'
|
64
|
+
- - ">="
|
65
|
+
- !ruby/object:Gem::Version
|
66
|
+
version: 3.3.3
|
67
|
+
- !ruby/object:Gem::Dependency
|
68
|
+
name: pry-rails
|
69
|
+
requirement: !ruby/object:Gem::Requirement
|
70
|
+
requirements:
|
71
|
+
- - "~>"
|
72
|
+
- !ruby/object:Gem::Version
|
73
|
+
version: '0.3'
|
74
|
+
- - ">="
|
75
|
+
- !ruby/object:Gem::Version
|
76
|
+
version: 0.3.4
|
77
|
+
type: :development
|
78
|
+
prerelease: false
|
79
|
+
version_requirements: !ruby/object:Gem::Requirement
|
80
|
+
requirements:
|
81
|
+
- - "~>"
|
82
|
+
- !ruby/object:Gem::Version
|
83
|
+
version: '0.3'
|
84
|
+
- - ">="
|
85
|
+
- !ruby/object:Gem::Version
|
86
|
+
version: 0.3.4
|
87
|
+
- !ruby/object:Gem::Dependency
|
88
|
+
name: database_cleaner
|
89
|
+
requirement: !ruby/object:Gem::Requirement
|
90
|
+
requirements:
|
91
|
+
- - "~>"
|
92
|
+
- !ruby/object:Gem::Version
|
93
|
+
version: '1.5'
|
94
|
+
- - ">="
|
95
|
+
- !ruby/object:Gem::Version
|
96
|
+
version: 1.5.1
|
97
|
+
type: :development
|
98
|
+
prerelease: false
|
99
|
+
version_requirements: !ruby/object:Gem::Requirement
|
100
|
+
requirements:
|
101
|
+
- - "~>"
|
102
|
+
- !ruby/object:Gem::Version
|
103
|
+
version: '1.5'
|
104
|
+
- - ">="
|
105
|
+
- !ruby/object:Gem::Version
|
106
|
+
version: 1.5.1
|
107
|
+
description: A scribe for your database and Rails project, Penman records your DB
|
108
|
+
changes and produces seed files that reflect them.
|
109
|
+
email:
|
110
|
+
- matpataki@gmail.com
|
111
|
+
executables: []
|
112
|
+
extensions: []
|
113
|
+
extra_rdoc_files: []
|
114
|
+
files:
|
115
|
+
- MIT-LICENSE
|
116
|
+
- Rakefile
|
117
|
+
- config/routes.rb
|
118
|
+
- db/migrate/20150909222413_create_record_tags.rb
|
119
|
+
- lib/penman.rb
|
120
|
+
- lib/penman/configuration.rb
|
121
|
+
- lib/penman/engine.rb
|
122
|
+
- lib/penman/penman_exceptions.rb
|
123
|
+
- lib/penman/record_tag.rb
|
124
|
+
- lib/penman/seed_code.rb
|
125
|
+
- lib/penman/seed_file_generator.rb
|
126
|
+
- lib/penman/taggable.rb
|
127
|
+
- lib/penman/version.rb
|
128
|
+
- lib/tasks/penman_tasks.rake
|
129
|
+
- lib/templates/default.rb.erb
|
130
|
+
homepage: http://uken.com
|
131
|
+
licenses:
|
132
|
+
- MIT
|
133
|
+
metadata: {}
|
134
|
+
post_install_message:
|
135
|
+
rdoc_options: []
|
136
|
+
require_paths:
|
137
|
+
- lib
|
138
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
139
|
+
requirements:
|
140
|
+
- - ">="
|
141
|
+
- !ruby/object:Gem::Version
|
142
|
+
version: '0'
|
143
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
144
|
+
requirements:
|
145
|
+
- - ">="
|
146
|
+
- !ruby/object:Gem::Version
|
147
|
+
version: '0'
|
148
|
+
requirements: []
|
149
|
+
rubyforge_project:
|
150
|
+
rubygems_version: 2.4.5.1
|
151
|
+
signing_key:
|
152
|
+
specification_version: 4
|
153
|
+
summary: Tracks database changes and generates representative seed files.
|
154
|
+
test_files: []
|