application_seeds 0.0.1

Sign up to get free protection for your applications and to get access to all the features.
data/.gitignore ADDED
@@ -0,0 +1,18 @@
1
+ *.gem
2
+ *.rbc
3
+ .bundle
4
+ .config
5
+ .ruby-version
6
+ .yardoc
7
+ Gemfile.lock
8
+ InstalledFiles
9
+ _yardoc
10
+ coverage
11
+ doc/
12
+ lib/bundler/man
13
+ pkg
14
+ rdoc
15
+ spec/reports
16
+ test/tmp
17
+ test/version_tmp
18
+ tmp
data/Gemfile ADDED
@@ -0,0 +1,4 @@
1
+ source 'https://rubygems.org'
2
+
3
+ # Specify your gem's dependencies in application_seeds.gemspec
4
+ gemspec
data/README.md ADDED
@@ -0,0 +1,319 @@
1
+ # application_seeds
2
+
3
+ A library for managing a standardized set of seed data for applications
4
+ in a non-production environment.
5
+
6
+
7
+ ## Requirements
8
+
9
+ * Postgresql - This library currently only works with the Postgresql
10
+ database.
11
+
12
+
13
+ ## Usage
14
+
15
+ #### Include the gem in your Gemfile
16
+
17
+ group :development, :test, :integration, :staging do
18
+ gem 'application_seeds', :git => 'git@github.com:centro/application_seeds.git'
19
+ end
20
+
21
+
22
+ #### Create a rake task to create data model objects from the seed data
23
+
24
+ **The application** needs to create objects from the common seed data. To
25
+ do this, the application will need to create Rake task (such as the one
26
+ below) that reads the seed data, and uses it to create the objects in
27
+ the application's own data model.
28
+
29
+ `ApplicationSeeds` provides an API to allow for the easy retrieveal of
30
+ seed data. See blow for more information about the API.
31
+
32
+ ```ruby
33
+ namespace :application_seeds do
34
+ desc 'Dump the development database and load it with standardized application seed data'
35
+ task :load, [:dataset] => ['db:drop', 'db:create', 'db:migrate', :environment] do |t, args|
36
+ ApplicationSeeds.dataset = args[:dataset]
37
+
38
+ seed_campaigns
39
+ seed_line_items
40
+ seed_some_other_objects
41
+ end
42
+
43
+ def seed_campaigns
44
+ # If we do not need to change the attirbute hash, we can just create
45
+ # the object with the attributes that are specified in the seed data
46
+ # file.
47
+ ApplicationSeeds.campaigns.each do |id, attributes|
48
+ ApplicationSeeds.create_object!(Campaign, id, attributes)
49
+ end
50
+ end
51
+
52
+ def seed_line_items
53
+ # If we need to reject attributes from the attribute hash, or
54
+ # only use specific attributes, we can use the select_attributes or
55
+ # the reject_attributes helper methods.
56
+ ApplicationSeeds.line_items.each do |id, attributes|
57
+ ApplicationSeeds.create_object!(LineItem, id, attributes.reject_attributes(:some_unused_attribute))
58
+ end
59
+ end
60
+
61
+ def seed_some_objects
62
+ # If we need to modify attribute names, we can do so using the
63
+ # map_attributes helper method.
64
+ ApplicationSeeds.some_objects.each do |id, attributes|
65
+ ApplicationSeeds.create_object!(SomeObject, id, attributes.map_attributes(
66
+ :old_name1 => :new_name1, :old_name2 => :new_name2))
67
+ end
68
+ end
69
+
70
+ def seed_some_other_objects
71
+ # If we need tighter control over how the object is created, we can
72
+ # simply create it ourselves.
73
+ ApplicationSeeds.some_other_objects.each do |id, attributes|
74
+ x = SomeOtherObject.new(param1: attributes['param1'],
75
+ param2: attributes['param2'],
76
+ param3: attributes['param3'])
77
+ x.id = id
78
+ x.save!
79
+ end
80
+ end
81
+ end
82
+ ```
83
+
84
+ #### Run the rake task
85
+
86
+ bundle exec rake application_seeds:load[your_data_set]
87
+
88
+ You must specify the seed data set that you would like to use. The dataset name is
89
+ simply the name of the directory containing the seed YAML files.
90
+
91
+
92
+ #### Or, run the capistrano task
93
+
94
+ Add the following line to your deploy.rb file:
95
+
96
+ require "application_seeds/capistrano"
97
+
98
+ Then, you can seed a remote database by running the following:
99
+
100
+ bundle exec cap <environment> deploy:application_seeds -s dataset=your_data_set
101
+
102
+
103
+ ## The API
104
+
105
+ The `ApplicationSeeds` module provides an API that enables the programmatic retrieval of seed data,
106
+ so the rake task can easily access all the seed data necessary to build the data object.
107
+
108
+
109
+ ### Specify the name of the directory containing the seed data
110
+
111
+ ```ruby
112
+ ApplicationSeeds.data_directory = "/path/to/seeds/directory"
113
+ ```
114
+
115
+ Specify the name of the directory that contains the application seed data.
116
+
117
+
118
+ ### Specify the name of the gem containing the seed data
119
+
120
+ ```ruby
121
+ ApplicationSeeds.data_gem_name = "my-seed-data-gem"
122
+ ```
123
+
124
+ Specify the name of the gem that contains the application seed data.
125
+ Defaults to `application_seed_data` if this method is not called.
126
+
127
+
128
+ ### Specify the dataset to be loaded
129
+
130
+ ```ruby
131
+ ApplicationSeeds.dataset = "name_of_your_dataset"
132
+ ```
133
+
134
+ Specify the name of the dataset to use. An exception will be raised if
135
+ the dataset could not be found.
136
+
137
+
138
+ ### Determining the dataset that has been loaded
139
+
140
+ ```ruby
141
+ ApplicationSeeds.dataset
142
+ ```
143
+
144
+ Returns the name of the dataset that has been loaded, or nil if not
145
+ running an application_seeds dataset.
146
+
147
+
148
+ ### Checking if a seed file exists in the dataset
149
+
150
+ ```ruby
151
+ ApplicationSeeds.seed_data_exists?(:campaigns)
152
+ ```
153
+
154
+ Returns `true` if `campaigns.yml` exists in this dataset, `false` if it
155
+ does not.
156
+
157
+
158
+ ### Fetching all seeds of a given type
159
+
160
+ ```ruby
161
+ ApplicationSeeds.campaigns # where "campaigns" is the name of the seed file
162
+ ```
163
+
164
+ This call returns a hash with one or more entries (depending on the contentes of the seed file).
165
+ The IDs of the object are the keys, and a hash containing the object's attributes are the values.
166
+ An exception is raised if no seed data could be with the given name.
167
+
168
+
169
+ ### Fetching seed data by ID
170
+
171
+ ```ruby
172
+ ApplicationSeeds.campaigns(1) # where "campaigns" is the name of the seed file, and 1 is the ID of the campaign
173
+ ```
174
+
175
+ This call returns a hash containing the object's attributes. An exception is raised if no
176
+ seed data could be found with the given ID.
177
+
178
+
179
+ ### Fetching seed data by some other attribute
180
+
181
+ ```ruby
182
+ ApplicationSeeds.campaigns(foo: 'bar', name: 'John') # where "campaigns" is the name of the seed file
183
+ ```
184
+
185
+ This call returns the seed data that contains the specified attributes,
186
+ and the specified attribute values. It returns a hash with zero or more
187
+ entries. The IDs of the object are the keys of the hash, and a hash
188
+ containing the object's attributes are the values. Any empty hash will
189
+ be returned if no seed data could be found with the given attribute names
190
+ and values.
191
+
192
+
193
+ ### Creating an object
194
+
195
+ ```ruby
196
+ ApplicationSeeds.create_object!(Campaign, id, attributes)
197
+ ```
198
+
199
+ This call will create a new instance of the `Campaign` class, with the
200
+ specified id and attributes.
201
+
202
+
203
+ ### Rejecting specific attributes
204
+
205
+ ```ruby
206
+ ApplicationSeeds.create_object!(Campaign, id, attributes.reject_attributes(:unused_attribute))
207
+ ```
208
+
209
+ This call will create a new instance of the `Campaign` class without the
210
+ `unused_attribute` attribute.
211
+
212
+
213
+ ### Selecting specific attributes
214
+
215
+ ```ruby
216
+ ApplicationSeeds.create_object!(Campaign, id, attributes.select_attributes(:attribute1, :attribute2))
217
+ ```
218
+
219
+ This call will create a new instance of the `Campaign` class with only the
220
+ `attribute1` and `attribute2` attributes.
221
+
222
+
223
+ ### Mapping attribute names
224
+
225
+ ```ruby
226
+ ApplicationSeeds.create_object!(Campaign, id, attributes.map_attributes(
227
+ :old_name1 => :new_name1, :old_name2 => :new_name2))
228
+ ```
229
+
230
+ This call will create a new instance of the `Campaign` class, using the
231
+ seed data for old_name1 as the attribute value for new_name1, and the
232
+ seed data for old_name2 as the attribute value for new_name2. This
233
+ method let's you easly account for slight differences is attribute names
234
+ across applications.
235
+
236
+
237
+ ### Reset id column sequence numbers
238
+
239
+ ```ruby
240
+ ApplicationSeeds.reset_sequence_numbers
241
+ ```
242
+
243
+ This method will reset the sequence numbers on id columns for all tables
244
+ in the database with an id column. If you are having issues where you
245
+ are unable to insert new data into the databse after your dataset has
246
+ been imported, then this should correct them.
247
+
248
+
249
+ ## The Problem
250
+
251
+ Applications in a service oriented architecture (SOA) are often
252
+ interconnected. One of the challenges with a SOA is that, since the
253
+ applications are (and must be to some extent) all interconnected, the
254
+ data sets used by the different applications must be *in sync*.
255
+
256
+ Applications will need to store keys to data in other applications that
257
+ can be used to fetch more detailed information from the services that
258
+ own that data. In order for one application to lookup data owned by
259
+ another application, the key specified by the client must be in the server's
260
+ data set, along with the other data associated with the key that the
261
+ client is requesting.
262
+
263
+ Often, each application will have its own, sioled seed data, making
264
+ inter-app communication impossible. In order to get all of the
265
+ application data in sync, developers will often resort to populating
266
+ their development databases with production data. Production data on a
267
+ developer machine (*especially* a laptop) is bad business. Do you want
268
+ to send the email to all of your customers telling them that their
269
+ sensitive data was on a stolen laptop? I didn't think so.
270
+
271
+
272
+ ## The Goal
273
+
274
+ The goal of this project is to create a common set of seed data that can
275
+ be used by all applications running in development. Re-seeding
276
+ the applications in development with this shared seed data would put them
277
+ all "on the same page", preparing them for inter-app communication.
278
+
279
+ The seed data would be in a general format, not formatted to any
280
+ application's data model. Each application will have a script that
281
+ mutates this seed data to confirm to its data model, and then persist it
282
+ to its database.
283
+
284
+
285
+ ## FAQ
286
+
287
+ #### Why not just stub calls to the respective services?
288
+
289
+ Easier said than done :) Yes, it would be fantastic if we could run an
290
+ application in isolation, and everything just works. But maintaining
291
+ the stubs can be difficult. Also, when you stub out service calls,
292
+ you're not really testing the inter-app communication process. More importantly,
293
+ stubbing out the calls really only works for read-only APIs. For APIs that
294
+ create or mutate data, stubbing isn't an ideal strategy. What happens
295
+ when the app tries to fetch data that it just created/updated on a remote
296
+ service? How will you see the data you created/updated?
297
+
298
+
299
+ #### Doesn't this mean that I need all applications running, all of the time?
300
+
301
+ Not really. But, you will need to be running the applications that
302
+ service API calls for whatever it is that you are developing/testing.
303
+ This is where [POW](http://pow.cx/) comes in. POW is a zero-config Rack
304
+ server for OSX. After installing POW, your apps will be accessible via a
305
+ .dev url, like http://myapp.dev No more remembering to
306
+ start an application before you use one of its services. No more
307
+ remembering which applications run on which ports. If your application
308
+ is not currently running, POW will start it automatically on the fly.
309
+
310
+
311
+ #### Sounds great, what's the catch?
312
+
313
+ Making it easier for our applications to talk to one another does have
314
+ some disadvantages. One being that it makes it easier to
315
+ couple applications. The goal of a service oriented
316
+ architecture is to prevent this. With great power comes great
317
+ responsibility. Carefully consider the trade offs any time you
318
+ introduce an API call to fetch data from a remote service.
319
+
data/Rakefile ADDED
@@ -0,0 +1 @@
1
+ require "bundler/gem_tasks"
@@ -0,0 +1,25 @@
1
+ # -*- encoding: utf-8 -*-
2
+ lib = File.expand_path('../lib', __FILE__)
3
+ $LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
4
+ require 'application_seeds/version'
5
+
6
+ Gem::Specification.new do |gem|
7
+ gem.name = "application_seeds"
8
+ gem.version = ApplicationSeeds::VERSION
9
+ gem.authors = ["John Wood"]
10
+ gem.email = ["john.wood@centro.net"]
11
+ gem.description = %q{A library for managing standardized application seed data}
12
+ gem.summary = %q{A library for managing a standardized set of seed data for applications in a non-production environment}
13
+ gem.homepage = "https://github.com/centro/application_seeds"
14
+
15
+ gem.files = `git ls-files`.split($/)
16
+ gem.executables = gem.files.grep(%r{^bin/}).map{ |f| File.basename(f) }
17
+ gem.test_files = gem.files.grep(%r{^(test|spec|features)/})
18
+ gem.require_paths = ["lib"]
19
+
20
+ gem.add_dependency "activesupport"
21
+ gem.add_dependency "pg"
22
+
23
+ gem.add_development_dependency "rspec"
24
+ gem.add_development_dependency "rake"
25
+ end
@@ -0,0 +1,35 @@
1
+ require 'delegate'
2
+
3
+ module ApplicationSeeds
4
+ class Attributes < DelegateClass(Hash)
5
+
6
+ def initialize(attributes)
7
+ super(attributes)
8
+ end
9
+
10
+ def select_attributes(*attribute_names)
11
+ attribute_names.map!(&:to_s)
12
+ select { |k,v| attribute_names.include?(k) }
13
+ end
14
+
15
+ def reject_attributes(*attribute_names)
16
+ attribute_names.map!(&:to_s)
17
+ reject { |k,v| attribute_names.include?(k) }
18
+ end
19
+
20
+ def map_attributes(mapping)
21
+ mapping.stringify_keys!
22
+
23
+ mapped = {}
24
+ each do |k,v|
25
+ if mapping.keys.include?(k)
26
+ mapped[mapping[k].to_s] = v
27
+ else
28
+ mapped[k] = v
29
+ end
30
+ end
31
+ Attributes.new(mapped)
32
+ end
33
+
34
+ end
35
+ end
@@ -0,0 +1,30 @@
1
+ require 'capistrano'
2
+
3
+ module ApplicationSeeds
4
+ module Capistrano
5
+
6
+ def self.load_into(configuration)
7
+ configuration.load do
8
+ set :dataset, ""
9
+
10
+ namespace :deploy do
11
+ task :application_seeds do
12
+ raise "You cannot run this task in the production environment" if rails_env == "production"
13
+
14
+ if dataset == ""
15
+ run %Q{cd #{latest_release} && #{rake} RAILS_ENV=#{rails_env} db:seed}
16
+ else
17
+ run %Q{cd #{latest_release} && #{rake} RAILS_ENV=#{rails_env} application_seeds:load\[#{dataset}\]}
18
+ end
19
+ end
20
+ end
21
+ end
22
+ end
23
+
24
+ end
25
+ end
26
+
27
+ if Capistrano::Configuration.instance
28
+ ApplicationSeeds::Capistrano.load_into(Capistrano::Configuration.instance)
29
+ end
30
+
@@ -0,0 +1,27 @@
1
+ module ApplicationSeeds
2
+ class Database
3
+
4
+ class << self
5
+ def connection
6
+ return @connection unless @connection.nil?
7
+
8
+ database_config = YAML.load(ERB.new(File.read("config/database.yml")).result)[Rails.env]
9
+
10
+ pg_config = {}
11
+ pg_config[:dbname] = database_config['database']
12
+ pg_config[:host] = database_config['host'] if database_config['host']
13
+ pg_config[:port] = database_config['port'] if database_config['port']
14
+ pg_config[:user] = database_config['username'] if database_config['username']
15
+ pg_config[:password] = database_config['password'] if database_config['password']
16
+
17
+ @connection = PG.connect(pg_config)
18
+ end
19
+
20
+ def create_metadata_table
21
+ connection.exec('DROP TABLE IF EXISTS application_seeds;')
22
+ connection.exec('CREATE TABLE application_seeds (dataset varchar(255));')
23
+ end
24
+ end
25
+
26
+ end
27
+ end
@@ -0,0 +1,3 @@
1
+ module ApplicationSeeds
2
+ VERSION = "0.0.1"
3
+ end
@@ -0,0 +1,255 @@
1
+ require "yaml"
2
+ require "erb"
3
+ require "pg"
4
+ require "active_support"
5
+ require "active_support/core_ext"
6
+
7
+ require "application_seeds/database"
8
+ require "application_seeds/version"
9
+ require "application_seeds/attributes"
10
+
11
+ # A library for managing a standardized set of seed data for applications in a non-production environment.
12
+ #
13
+ # == The API
14
+ #
15
+ # === Fetching all seeds of a given type
16
+ #
17
+ # ApplicationSeeds.campaigns # where "campaigns" is the name of the seed file
18
+ #
19
+ # This call returns a hash with one or more entries (depending on the contentes of the seed file).
20
+ # The IDs of the object are the keys, and a hash containing the object's attributes are the values.
21
+ # An exception is raised if no seed data could be with the given name.
22
+ #
23
+ # === Fetching seed data by ID
24
+ #
25
+ # ApplicationSeeds.campaigns(1) # where "campaigns" is the name of the seed file, and 1 is the ID of the campaign
26
+ #
27
+ # This call returns a hash containing the object's attributes. An exception is raised if no
28
+ # seed data could be found with the given ID.
29
+ #
30
+ # === Fetching seed data by some other attribute
31
+ #
32
+ # ApplicationSeeds.campaigns(foo: 'bar', name: 'John') # where "campaigns" is the name of the seed file
33
+ #
34
+ # This call returns the seed data that contains the specified attributes,
35
+ # and the specified attribute values. It returns a hash with zero or more
36
+ # entries. The IDs of the object are the keys of the hash, and a hash
37
+ # containing the object's attributes are the values. Any empty hash will
38
+ # be returned if no seed data could be found with the given attribute names
39
+ # and values.
40
+ #
41
+ # === Creating an object
42
+ #
43
+ # ApplicationSeeds.create_object!(Campaign, id, attributes)
44
+ #
45
+ # This call will create a new instance of the <tt>Campaign</tt> class, with the
46
+ # specified id and attributes.
47
+ #
48
+ # === Rejecting specific attributes
49
+ #
50
+ # ApplicationSeeds.create_object!(Campaign, id, attributes.reject_attributes(:unused_attribute))
51
+ #
52
+ # This call will create a new instance of the <tt>Campaign</tt> class without the
53
+ # <tt>unused_attribute</tt> attribute.
54
+ #
55
+ # === Selecting specific attributes
56
+ #
57
+ # ApplicationSeeds.create_object!(Campaign, id, attributes.select_attributes(:attribute1, :attribute2))
58
+ #
59
+ # This call will create a new instance of the <tt>Campaign</tt> class with only the
60
+ # <tt>attribute1</tt> and <tt>attribute2</tt> attributes.
61
+ #
62
+ # === Mapping attribute names
63
+ #
64
+ # ApplicationSeeds.create_object!(Campaign, id, attributes.map_attributes(
65
+ # :old_name1 => :new_name1, :old_name2 => :new_name2))
66
+ #
67
+ # This call will create a new instance of the <tt>Campaign</tt> class, using the
68
+ # seed data for old_name1 as the attribute value for new_name1, and the
69
+ # seed data for old_name2 as the attribute value for new_name2. This
70
+ # method let's you easly account for slight differences is attribute names
71
+ # across applications.
72
+ #
73
+ module ApplicationSeeds
74
+ class << self
75
+
76
+ #
77
+ # Specify the name of the gem that contains the application seed data.
78
+ #
79
+ def data_gem_name=(gem_name)
80
+ spec = Gem::Specification.find_by_name(gem_name)
81
+ if Dir.exist?(File.join(spec.gem_dir, "lib", "seeds"))
82
+ @data_gem_name = gem_name
83
+ else
84
+ raise "ERROR: The #{gem_name} gem does not appear to contain application seed data"
85
+ end
86
+ end
87
+
88
+ #
89
+ # Fetch the name of the directory where the application seed data is loaded from.
90
+ # Defaults to <tt>"applicadtion_seed_data"</tt> if it was not set using <tt>data_gem_name=</tt>.
91
+ #
92
+ def data_gem_name
93
+ @data_gem_name || "application_seed_data"
94
+ end
95
+
96
+ #
97
+ # Specify the name of the directory that contains the application seed data.
98
+ #
99
+ def data_directory=(directory)
100
+ if Dir.exist?(directory)
101
+ @data_directory = directory
102
+ else
103
+ raise "ERROR: The #{directory} directory does not appear to contain application seed data"
104
+ end
105
+ end
106
+
107
+ #
108
+ # Fetch the name of the directory where the application seed data is loaded from,
109
+ # if it was set using <tt>data_diretory=</tt>.
110
+ #
111
+ def data_directory
112
+ @data_directory
113
+ end
114
+
115
+ #
116
+ # Specify the name of the dataset to use. An exception will be raised if
117
+ # the dataset could not be found.
118
+ #
119
+ def dataset=(dataset)
120
+ if dataset.nil? || dataset.strip.empty? || !Dir.exist?(File.join(seed_data_path, dataset))
121
+ datasets = Dir[File.join(seed_data_path, "*")].map { |x| File.basename(x) }.join(', ')
122
+
123
+ error_message = "\nERROR: A valid dataset is required!\n"
124
+ error_message << "Usage: bundle exec rake application_seeds:load[your_data_set]\n\n"
125
+ error_message << "Available datasets: #{datasets}\n\n"
126
+ raise error_message
127
+ end
128
+
129
+ Database.create_metadata_table
130
+ Database.connection.exec("INSERT INTO application_seeds (dataset) VALUES ('#{dataset}');")
131
+
132
+ @dataset = dataset
133
+ end
134
+
135
+ #
136
+ # Returns the name of the dataset that has been loaded, or nil if not
137
+ # running an application_seeds dataset.
138
+ #
139
+ def dataset
140
+ res = Database.connection.exec("SELECT dataset from application_seeds LIMIT 1;")
141
+ res.getvalue(0, 0)
142
+ rescue PG::Error => e
143
+ e.message =~ /relation "application_seeds" does not exist/ ? nil : raise
144
+ end
145
+
146
+ #
147
+ # This call will create a new instance of the specified class, with the
148
+ # specified id and attributes.
149
+ #
150
+ def create_object!(clazz, id, attributes, options={})
151
+ validate = options[:validate].nil? ? true : options[:validate]
152
+
153
+ x = clazz.new
154
+ x.attributes = attributes.reject { |k,v| !x.respond_to?("#{k}=") }
155
+ x.id = id
156
+ x.save!(:validate => validate)
157
+ x
158
+ end
159
+
160
+ #
161
+ # Returns <tt>true</tt> if the specified data file exists in this dataset, <tt>false</tt> if it
162
+ # does not.
163
+ #
164
+ # Examples:
165
+ # ApplicationSeeds.seed_data_exists?(:campaigns)
166
+ #
167
+ def seed_data_exists?(type)
168
+ File.exist?(File.join(seed_data_path, @dataset, "#{type}.yml"))
169
+ end
170
+
171
+ #
172
+ # This method will reset the sequence numbers on id columns for all tables
173
+ # in the database with an id column. If you are having issues where you
174
+ # are unable to insert new data into the databse after your dataset has
175
+ # been imported, then this should correct them.
176
+ #
177
+ def reset_sequence_numbers
178
+ result = Database.connection.exec("SELECT table_name FROM information_schema.tables WHERE table_schema = 'public';")
179
+ table_names = result.map { |row| row.values_at('table_name')[0] }
180
+
181
+ table_names_with_id_column = table_names.select do |table_name|
182
+ result = Database.connection.exec("SELECT column_name FROM information_schema.columns WHERE table_name = '#{table_name}';")
183
+ column_names = result.map { |row| row.values_at('column_name')[0] }
184
+ column_names.include?('id')
185
+ end
186
+
187
+ table_names_with_id_column.each do |table_name|
188
+ result = Database.connection.exec("SELECT pg_get_serial_sequence('#{table_name}', 'id');")
189
+ sequence_name = result.getvalue(0, 0)
190
+ Database.connection.exec("SELECT setval('#{sequence_name}', (select MAX(id) from #{table_name}));")
191
+ end
192
+ end
193
+
194
+ private
195
+
196
+ def method_missing(method, *args)
197
+ self.send(:seed_data, method, args.shift)
198
+ end
199
+
200
+ def seed_data(type, options)
201
+ @seed_data ||= {}
202
+ @seed_data[type] ||= load_seed_data(type)
203
+ raise "No seed data could be found for '#{type}'" if @seed_data[type].nil?
204
+
205
+ if options.nil?
206
+ fetch(type)
207
+ elsif options.is_a?(Fixnum) || options.is_a?(String)
208
+ fetch_with_id(type, options)
209
+ elsif options.is_a? Hash
210
+ fetch(type) do |attributes|
211
+ (options.stringify_keys.to_a - attributes.to_a).empty?
212
+ end
213
+ end
214
+ end
215
+
216
+ def load_seed_data(type)
217
+ data_file = File.join(seed_data_path, @dataset, "#{type}.yml")
218
+ if File.exist?(data_file)
219
+ YAML.load(ERB.new(File.read(data_file)).result)
220
+ else
221
+ nil
222
+ end
223
+ end
224
+
225
+ def seed_data_path
226
+ return @seed_data_path unless @seed_data_path.nil?
227
+
228
+ if data_directory
229
+ @seed_data_path = data_directory
230
+ else
231
+ spec = Gem::Specification.find_by_name(data_gem_name)
232
+ @seed_data_path = File.join(spec.gem_dir, "lib", "seeds")
233
+ end
234
+ end
235
+
236
+ def fetch(type, &block)
237
+ result = {}
238
+ @seed_data[type].each do |d|
239
+ attributes = d.clone
240
+ id = attributes.delete('id')
241
+ if !block_given? || (block_given? && yield(attributes) == true)
242
+ result[id] = Attributes.new(attributes)
243
+ end
244
+ end
245
+ result
246
+ end
247
+
248
+ def fetch_with_id(type, id)
249
+ data = @seed_data[type].find { |d| d['id'].to_s == id.to_s }
250
+ raise "No seed data could be found for '#{type}' with id #{id}" if data.nil?
251
+ Attributes.new(data)
252
+ end
253
+
254
+ end
255
+ end
@@ -0,0 +1,83 @@
1
+ require 'application_seeds'
2
+
3
+ describe "ApplicationSeeds" do
4
+ before do
5
+ ApplicationSeeds.data_directory = File.join(File.dirname(__FILE__), "seed_data")
6
+ end
7
+
8
+ describe "#data_gem_name=" do
9
+ it "raises an error if no gem could be found with the specified name" do
10
+ expect { ApplicationSeeds.data_gem_name = "foo" }.to raise_error(Gem::LoadError)
11
+ end
12
+ it "raises an error if the specified gem does not contain seed data" do
13
+ expect { ApplicationSeeds.data_gem_name = "rspec" }.to raise_error(RuntimeError, /does not appear to contain application seed data/)
14
+ end
15
+ end
16
+
17
+ describe "#data_gem_name" do
18
+ it "defaults to 'application_seed_data'" do
19
+ ApplicationSeeds.data_gem_name.should == "application_seed_data"
20
+ end
21
+ end
22
+
23
+ describe "#data_directory" do
24
+ it "is able to set the data directory successfully" do
25
+ ApplicationSeeds.data_directory.should == File.join(File.dirname(__FILE__), "seed_data")
26
+ end
27
+ it "raises an error if a non-existant directory specified" do
28
+ expect { ApplicationSeeds.data_directory = "/foo/bar" }.to raise_error
29
+ end
30
+ end
31
+
32
+ describe "#dataset=" do
33
+ context "when an invalid dataset is specified" do
34
+ it "raises an error if a nil dataset is specified" do
35
+ expect { ApplicationSeeds.dataset = nil }.to raise_error
36
+ end
37
+ it "raises an error if a blank dataset is specified" do
38
+ expect { ApplicationSeeds.dataset = " " }.to raise_error
39
+ end
40
+ it "raises an error if an unknown dataset is specified" do
41
+ expect { ApplicationSeeds.dataset = "foo" }.to raise_error
42
+ end
43
+ it "lists the available datasets in the error message" do
44
+ expect { ApplicationSeeds.dataset = nil }.to raise_error(RuntimeError, /Available datasets: test_data_set/)
45
+ end
46
+ end
47
+
48
+ context "when a valid dataset is specified" do
49
+ before do
50
+ connection_dummy = double
51
+ connection_dummy.should_receive(:exec).with("INSERT INTO application_seeds (dataset) VALUES ('test_data_set');")
52
+ ApplicationSeeds::Database.should_receive(:create_metadata_table)
53
+ ApplicationSeeds::Database.should_receive(:connection) { connection_dummy }
54
+ ApplicationSeeds.dataset = "test_data_set"
55
+ end
56
+ it "sets the dataset" do
57
+ ApplicationSeeds.instance_variable_get(:@dataset).should == "test_data_set"
58
+ end
59
+ end
60
+ end
61
+
62
+ describe "#dataset" do
63
+ before do
64
+ connection_dummy = double
65
+ response_dummy = double(:getvalue => "test_data_set")
66
+ connection_dummy.should_receive(:exec).with("SELECT dataset from application_seeds LIMIT 1;") { response_dummy }
67
+ ApplicationSeeds::Database.should_receive(:connection) { connection_dummy }
68
+ end
69
+ it "fetches the dataset name from the database" do
70
+ ApplicationSeeds.dataset.should == "test_data_set"
71
+ end
72
+ end
73
+
74
+ describe "#seed_data_exists?" do
75
+ it "returns true if the specified seed data exists" do
76
+ ApplicationSeeds.seed_data_exists?(:people).should be_true
77
+ end
78
+ it "returns false if the specified seed data does not exist" do
79
+ ApplicationSeeds.seed_data_exists?(:missing).should_not be_true
80
+ end
81
+ end
82
+
83
+ end
@@ -0,0 +1,18 @@
1
+ - id: 1
2
+ first_name: Joe
3
+ last_name: Smith
4
+ company_id: 1
5
+ start_date: <%= 2.months.ago.to_date %>
6
+
7
+ - id: 2
8
+ first_name: Jane
9
+ last_name: Doe
10
+ company_id: 1
11
+ start_date: <%= 10.months.ago.to_date %>
12
+
13
+ - id: 3
14
+ first_name: John
15
+ last_name: Walsh
16
+ company_id: 2
17
+ start_date: <%= 10.years.ago.to_date %>
18
+
metadata ADDED
@@ -0,0 +1,130 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: application_seeds
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.0.1
5
+ prerelease:
6
+ platform: ruby
7
+ authors:
8
+ - John Wood
9
+ autorequire:
10
+ bindir: bin
11
+ cert_chain: []
12
+ date: 2013-07-26 00:00:00.000000000 Z
13
+ dependencies:
14
+ - !ruby/object:Gem::Dependency
15
+ name: activesupport
16
+ requirement: !ruby/object:Gem::Requirement
17
+ none: false
18
+ requirements:
19
+ - - ! '>='
20
+ - !ruby/object:Gem::Version
21
+ version: '0'
22
+ type: :runtime
23
+ prerelease: false
24
+ version_requirements: !ruby/object:Gem::Requirement
25
+ none: false
26
+ requirements:
27
+ - - ! '>='
28
+ - !ruby/object:Gem::Version
29
+ version: '0'
30
+ - !ruby/object:Gem::Dependency
31
+ name: pg
32
+ requirement: !ruby/object:Gem::Requirement
33
+ none: false
34
+ requirements:
35
+ - - ! '>='
36
+ - !ruby/object:Gem::Version
37
+ version: '0'
38
+ type: :runtime
39
+ prerelease: false
40
+ version_requirements: !ruby/object:Gem::Requirement
41
+ none: false
42
+ requirements:
43
+ - - ! '>='
44
+ - !ruby/object:Gem::Version
45
+ version: '0'
46
+ - !ruby/object:Gem::Dependency
47
+ name: rspec
48
+ requirement: !ruby/object:Gem::Requirement
49
+ none: false
50
+ requirements:
51
+ - - ! '>='
52
+ - !ruby/object:Gem::Version
53
+ version: '0'
54
+ type: :development
55
+ prerelease: false
56
+ version_requirements: !ruby/object:Gem::Requirement
57
+ none: false
58
+ requirements:
59
+ - - ! '>='
60
+ - !ruby/object:Gem::Version
61
+ version: '0'
62
+ - !ruby/object:Gem::Dependency
63
+ name: rake
64
+ requirement: !ruby/object:Gem::Requirement
65
+ none: false
66
+ requirements:
67
+ - - ! '>='
68
+ - !ruby/object:Gem::Version
69
+ version: '0'
70
+ type: :development
71
+ prerelease: false
72
+ version_requirements: !ruby/object:Gem::Requirement
73
+ none: false
74
+ requirements:
75
+ - - ! '>='
76
+ - !ruby/object:Gem::Version
77
+ version: '0'
78
+ description: A library for managing standardized application seed data
79
+ email:
80
+ - john.wood@centro.net
81
+ executables: []
82
+ extensions: []
83
+ extra_rdoc_files: []
84
+ files:
85
+ - .gitignore
86
+ - Gemfile
87
+ - README.md
88
+ - Rakefile
89
+ - application_seeds.gemspec
90
+ - lib/application_seeds.rb
91
+ - lib/application_seeds/attributes.rb
92
+ - lib/application_seeds/capistrano.rb
93
+ - lib/application_seeds/database.rb
94
+ - lib/application_seeds/version.rb
95
+ - spec/application_seeds_spec.rb
96
+ - spec/seed_data/test_data_set/people.yml
97
+ homepage: https://github.com/centro/application_seeds
98
+ licenses: []
99
+ post_install_message:
100
+ rdoc_options: []
101
+ require_paths:
102
+ - lib
103
+ required_ruby_version: !ruby/object:Gem::Requirement
104
+ none: false
105
+ requirements:
106
+ - - ! '>='
107
+ - !ruby/object:Gem::Version
108
+ version: '0'
109
+ segments:
110
+ - 0
111
+ hash: -2100833736178153226
112
+ required_rubygems_version: !ruby/object:Gem::Requirement
113
+ none: false
114
+ requirements:
115
+ - - ! '>='
116
+ - !ruby/object:Gem::Version
117
+ version: '0'
118
+ segments:
119
+ - 0
120
+ hash: -2100833736178153226
121
+ requirements: []
122
+ rubyforge_project:
123
+ rubygems_version: 1.8.23
124
+ signing_key:
125
+ specification_version: 3
126
+ summary: A library for managing a standardized set of seed data for applications in
127
+ a non-production environment
128
+ test_files:
129
+ - spec/application_seeds_spec.rb
130
+ - spec/seed_data/test_data_set/people.yml