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 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,2 @@
1
+ Rails.application.routes.draw do
2
+ end
@@ -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
@@ -0,0 +1,7 @@
1
+ module Penman
2
+ MAJOR = 0 # api
3
+ MINOR = 4 # features
4
+ PATCH = 9 # bug fixes
5
+
6
+ VERSION = [MAJOR, MINOR, PATCH].compact.join('.')
7
+ 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
@@ -0,0 +1,6 @@
1
+ # namespace :penman do
2
+ # desc 'Initialize Penman'
3
+ # task :init do
4
+
5
+ # end
6
+ # end
@@ -0,0 +1,7 @@
1
+ # generated by penman
2
+
3
+ class <%= file_name.camelize %> < ActiveRecord::Migration
4
+ def change
5
+ <%= seed_code.print_with_leading_spaces(4) %>
6
+ end
7
+ 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: []