application_seeds 0.0.1
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.
- data/.gitignore +18 -0
- data/Gemfile +4 -0
- data/README.md +319 -0
- data/Rakefile +1 -0
- data/application_seeds.gemspec +25 -0
- data/lib/application_seeds/attributes.rb +35 -0
- data/lib/application_seeds/capistrano.rb +30 -0
- data/lib/application_seeds/database.rb +27 -0
- data/lib/application_seeds/version.rb +3 -0
- data/lib/application_seeds.rb +255 -0
- data/spec/application_seeds_spec.rb +83 -0
- data/spec/seed_data/test_data_set/people.yml +18 -0
- metadata +130 -0
data/.gitignore
ADDED
data/Gemfile
ADDED
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,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
|