penman 0.4.9
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/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: []
|