seed-fu 1.2.3 → 2.0.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,77 @@
1
+ require 'active_support/core_ext/hash/keys'
2
+
3
+ module SeedFu
4
+ # Creates or updates seed records with data.
5
+ #
6
+ # It is not recommended to use this class directly. Instead, use `Model.seed`, and `Model.seed_once`,
7
+ # where `Model` is your Active Record model.
8
+ #
9
+ # @see ActiveRecordExtension
10
+ class Seeder
11
+ # @param [ActiveRecord::Base] model_class The model to be seeded
12
+ # @param [Array<Symbol>] constraints A list of attributes which identify a particular seed. If
13
+ # a record with these attributes already exists then it will be updated rather than created.
14
+ # @param [Array<Hash>] data Each item in this array is a hash containing attributes for a
15
+ # particular record.
16
+ # @param [Hash] options
17
+ # @option options [Boolean] :quiet (SeedFu.quiet) If true, output will be silenced
18
+ # @option options [Boolean] :insert_only (false) If true then existing records which match the
19
+ # constraints will not be updated, even if the seed data has changed
20
+ def initialize(model_class, constraints, data, options = {})
21
+ @model_class = model_class
22
+ @constraints = constraints.to_a.empty? ? [:id] : constraints
23
+ @data = data.to_a || []
24
+ @options = options.symbolize_keys
25
+
26
+ @options[:quiet] ||= SeedFu.quiet
27
+
28
+ validate_constraints!
29
+ validate_data!
30
+ end
31
+
32
+ # Insert/update the records as appropriate. Validation is skipped while saving.
33
+ # @return [Array<ActiveRecord::Base>] The records which have been seeded
34
+ def seed
35
+ @model_class.transaction do
36
+ @data.map { |record_data| seed_record(record_data.symbolize_keys) }
37
+ end
38
+ end
39
+
40
+ private
41
+
42
+ def validate_constraints!
43
+ unknown_columns = @constraints.map(&:to_s) - @model_class.column_names
44
+ unless unknown_columns.empty?
45
+ raise(ArgumentError,
46
+ "Your seed constraints contained unknown columns: #{column_list(unknown_columns)}. " +
47
+ "Valid columns are: #{column_list(@model_class.column_names)}.")
48
+ end
49
+ end
50
+
51
+ def validate_data!
52
+ raise ArgumentError, "Seed data missing" if @data.empty?
53
+ end
54
+
55
+ def column_list(columns)
56
+ '`' + columns.join("`, `") + '`'
57
+ end
58
+
59
+ def seed_record(data)
60
+ record = find_or_initialize_record(data)
61
+ return if @options[:insert_only] && !record.new_record?
62
+ record.send(:attributes=, data, false)
63
+ puts " - #{@model_class} #{data.inspect}" unless @options[:quiet]
64
+ record.save(:validate => false)
65
+ record
66
+ end
67
+
68
+ def find_or_initialize_record(data)
69
+ @model_class.where(constraint_conditions(data)).first ||
70
+ @model_class.new
71
+ end
72
+
73
+ def constraint_conditions(data)
74
+ Hash[@constraints.map { |c| [c, data[c.to_sym]] }]
75
+ end
76
+ end
77
+ end
@@ -0,0 +1,4 @@
1
+ module SeedFu
2
+ # The current version of Seed Fu
3
+ VERSION = '2.0.0'
4
+ end
@@ -1,3 +1,132 @@
1
- require File.join( File.dirname(__FILE__), 'writer', 'abstract' )
2
- require File.join( File.dirname(__FILE__), 'writer', 'seed' )
3
- require File.join( File.dirname(__FILE__), 'writer', 'seed_many' )
1
+ module SeedFu
2
+ # {Writer} is used to programmatically generated seed files. For example, you might want to write
3
+ # a script which converts data in a CSV file to a valid Seed Fu seed file, which can then be
4
+ # imported.
5
+ #
6
+ # @example Basic usage
7
+ # SeedFu::Writer.write('path/to/file.rb', :class_name => 'Person', :constraints => [:first_name, :last_name]) do |writer|
8
+ # writer.write(:first_name => 'Jon', :last_name => 'Smith', :age => 21)
9
+ # writer.write(:first_name => 'Emily', :last_name => 'McDonald', :age => 24)
10
+ # end
11
+ #
12
+ # # Writes the following to the file:
13
+ # #
14
+ # # Person.seed(:first_name, :last_name,
15
+ # # {:first_name=>"Jon", :last_name=>"Smith", :age=>21},
16
+ # # {:first_name=>"Emily", :last_name=>"McDonald", :age=>24}
17
+ # # )
18
+ class Writer
19
+ cattr_accessor :default_options
20
+ @@default_options = {
21
+ :chunk_size => 100,
22
+ :constraints => [:id],
23
+ :seed_type => :seed
24
+ }
25
+
26
+ # @param [Hash] options
27
+ # @option options [String] :class_name *Required* The name of the Active Record model to
28
+ # generate seeds for
29
+ # @option options [Fixnum] :chunk_size (100) The number of seeds to write before generating a
30
+ # `# BREAK EVAL` line. (Chunking reduces memory usage when loading seeds.)
31
+ # @option options [:seed, :seed_once] :seed_type (:seed) The method to use when generating
32
+ # seeds. See {ActiveRecordExtension} for details.
33
+ # @option options [Array<Symbol>] :constraints ([:id]) The constraining attributes for the seeds
34
+ def initialize(options = {})
35
+ @options = self.class.default_options.merge(options)
36
+ raise ArgumentError, "missing option :class_name" unless @options[:class_name]
37
+ end
38
+
39
+ # Creates a new instance of {Writer} with the `options`, and then calls {#write} with the
40
+ # `io_or_filename` and `block`
41
+ def self.write(io_or_filename, options = {}, &block)
42
+ new(options).write(io_or_filename, &block)
43
+ end
44
+
45
+ # Writes the necessary headers and footers, and yields to a block within which the actual
46
+ # seed data should be writting using the `#<<` method.
47
+ #
48
+ # @param [IO] io_or_filename The IO to which writes will be made. (If an `IO` is given, it is
49
+ # your responsibility to close it after writing.)
50
+ # @param [String] io_or_filename The filename of a file to make writes to. (Will be opened and
51
+ # closed automatically.)
52
+ # @yield [self] make calls to `#<<` within the block
53
+ def write(io_or_filename, &block)
54
+ raise ArgumentError, "missing block" unless block_given?
55
+
56
+ if io_or_filename.respond_to?(:write)
57
+ write_to_io(io_or_filename, &block)
58
+ else
59
+ File.open(io_or_filename, 'w') do |file|
60
+ write_to_io(file, &block)
61
+ end
62
+ end
63
+ end
64
+
65
+ # Add a seed. Must be called within a block passed to {#write}.
66
+ # @param [Hash] seed The attributes for the seed
67
+ def <<(seed)
68
+ raise "You must add seeds inside a SeedFu::Writer#write block" unless @io
69
+
70
+ buffer = ''
71
+
72
+ if chunk_this_seed?
73
+ buffer << seed_footer
74
+ buffer << "# BREAK EVAL\n"
75
+ buffer << seed_header
76
+ end
77
+
78
+ buffer << ",\n"
79
+ buffer << ' ' + seed.inspect
80
+
81
+ @io.write(buffer)
82
+
83
+ @count += 1
84
+ end
85
+ alias_method :add, :<<
86
+
87
+ private
88
+
89
+ def write_to_io(io)
90
+ @io, @count = io, 0
91
+ @io.write(file_header)
92
+ @io.write(seed_header)
93
+ yield(self)
94
+ @io.write(seed_footer)
95
+ @io.write(file_footer)
96
+ ensure
97
+ @io, @count = nil, nil
98
+ end
99
+
100
+ def file_header
101
+ <<-END
102
+ # DO NOT MODIFY THIS FILE, it was auto-generated.
103
+ #
104
+ # Date: #{Time.now}
105
+ # Seeding #{@options[:class_name]}
106
+ # Written with the command:
107
+ #
108
+ # #{$0} #{$*.join}
109
+ #
110
+ END
111
+ end
112
+
113
+ def file_footer
114
+ <<-END
115
+ # End auto-generated file.
116
+ END
117
+ end
118
+
119
+ def seed_header
120
+ constraints = @options[:constraints] && @options[:constraints].map(&:inspect).join(', ')
121
+ "#{@options[:class_name]}.#{@options[:seed_type]}(#{constraints}"
122
+ end
123
+
124
+ def seed_footer
125
+ "\n)\n"
126
+ end
127
+
128
+ def chunk_this_seed?
129
+ @count != 0 && (@count % @options[:chunk_size]) == 0
130
+ end
131
+ end
132
+ end
@@ -0,0 +1,43 @@
1
+ require 'seed-fu'
2
+
3
+ namespace :db do
4
+ desc <<-EOS
5
+ Loads seed data for the current environment. It will look for
6
+ ruby seed files in <RAILS_ROOT>/db/fixtures/ and
7
+ <RAILS_ROOT>/db/fixtures/<RAILS_ENV>/.
8
+
9
+ By default it will load any ruby files found. You can filter the files
10
+ loaded by passing in the FILTER environment variable with a comma-delimited
11
+ list of patterns to include. Any files not matching the pattern will
12
+ not be loaded.
13
+
14
+ You can also change the directory where seed files are looked for
15
+ with the FIXTURE_PATH environment variable.
16
+
17
+ Examples:
18
+ # default, to load all seed files for the current environment
19
+ rake db:seed
20
+
21
+ # to load seed files matching orders or customers
22
+ rake db:seed FILTER=orders,customers
23
+
24
+ # to load files from RAILS_ROOT/features/fixtures
25
+ rake db:seed FIXTURE_PATH=features/fixtures
26
+ EOS
27
+ task :seed_fu => :environment do
28
+ if ENV["SEED"]
29
+ puts "DEPRECATED: The SEED option to db:seed_fu is deprecated. Please use FILTER instead."
30
+ ENV["FILTER"] = ENV["SEED"]
31
+ end
32
+
33
+ if ENV["FILTER"]
34
+ filter = /#{ENV["FILTER"].gsub(/,/, "|")}/
35
+ end
36
+
37
+ if ENV["FIXTURE_PATH"]
38
+ fixture_paths = [ENV["FIXTURE_PATH"], ENV["FIXTURE_PATH"] + '/' + Rails.env]
39
+ end
40
+
41
+ SeedFu.seed(fixture_paths, filter)
42
+ end
43
+ end
metadata CHANGED
@@ -1,77 +1,120 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: seed-fu
3
3
  version: !ruby/object:Gem::Version
4
- version: 1.2.3
4
+ prerelease: false
5
+ segments:
6
+ - 2
7
+ - 0
8
+ - 0
9
+ version: 2.0.0
5
10
  platform: ruby
6
11
  authors:
7
12
  - Michael Bleigh
13
+ - Jon Leighton
8
14
  autorequire:
9
15
  bindir: bin
10
16
  cert_chain: []
11
17
 
12
- date: 2010-01-20 00:00:00 -05:00
18
+ date: 2010-11-05 00:00:00 +00:00
13
19
  default_executable:
14
20
  dependencies:
15
21
  - !ruby/object:Gem::Dependency
16
- name: rails
22
+ name: activerecord
23
+ prerelease: false
24
+ requirement: &id001 !ruby/object:Gem::Requirement
25
+ none: false
26
+ requirements:
27
+ - - ~>
28
+ - !ruby/object:Gem::Version
29
+ segments:
30
+ - 3
31
+ - 0
32
+ - 0
33
+ version: 3.0.0
17
34
  type: :runtime
18
- version_requirement:
19
- version_requirements: !ruby/object:Gem::Requirement
35
+ version_requirements: *id001
36
+ - !ruby/object:Gem::Dependency
37
+ name: rspec
38
+ prerelease: false
39
+ requirement: &id002 !ruby/object:Gem::Requirement
40
+ none: false
41
+ requirements:
42
+ - - ~>
43
+ - !ruby/object:Gem::Version
44
+ segments:
45
+ - 2
46
+ - 0
47
+ - 0
48
+ version: 2.0.0
49
+ type: :development
50
+ version_requirements: *id002
51
+ - !ruby/object:Gem::Dependency
52
+ name: sqlite3
53
+ prerelease: false
54
+ requirement: &id003 !ruby/object:Gem::Requirement
55
+ none: false
20
56
  requirements:
21
57
  - - ">="
22
58
  - !ruby/object:Gem::Version
23
- version: "2.1"
24
- version:
59
+ segments:
60
+ - 0
61
+ version: "0"
62
+ type: :development
63
+ version_requirements: *id003
25
64
  description: Seed Fu is an attempt to once and for all solve the problem of inserting and maintaining seed data in a database. It uses a variety of techniques gathered from various places around the web and combines them to create what is hopefully the most robust seed data system around.
26
- email: michael@intridea.com
65
+ email:
66
+ - michael@intridea.com
67
+ - j@jonathanleighton.com
27
68
  executables: []
28
69
 
29
70
  extensions: []
30
71
 
31
- extra_rdoc_files:
32
- - README.rdoc
72
+ extra_rdoc_files: []
73
+
33
74
  files:
34
- - README.rdoc
35
- - Rakefile
36
- - VERSION
37
- - lib/seed-fu.rb
75
+ - lib/tasks/seed_fu.rake
76
+ - lib/seed-fu/block_hash.rb
77
+ - lib/seed-fu/runner.rb
78
+ - lib/seed-fu/active_record_extension.rb
79
+ - lib/seed-fu/seeder.rb
80
+ - lib/seed-fu/railtie.rb
81
+ - lib/seed-fu/version.rb
38
82
  - lib/seed-fu/writer.rb
39
- - lib/seed-fu/writer/abstract.rb
40
- - lib/seed-fu/writer/seed.rb
41
- - lib/seed-fu/writer/seed_many.rb
42
- - rails/init.rb
43
- - spec/schema.rb
44
- - spec/seed_fu_spec.rb
45
- - spec/spec_helper.rb
83
+ - lib/seed-fu.rb
84
+ - LICENSE
85
+ - README.md
86
+ - CHANGELOG.md
46
87
  has_rdoc: true
47
- homepage: http://github.com/mblegih/seed-fu
88
+ homepage: http://github.com/mbleigh/seed-fu
48
89
  licenses: []
49
90
 
50
91
  post_install_message:
51
- rdoc_options:
52
- - --charset=UTF-8
92
+ rdoc_options: []
93
+
53
94
  require_paths:
54
95
  - lib
55
96
  required_ruby_version: !ruby/object:Gem::Requirement
97
+ none: false
56
98
  requirements:
57
99
  - - ">="
58
100
  - !ruby/object:Gem::Version
101
+ segments:
102
+ - 0
59
103
  version: "0"
60
- version:
61
104
  required_rubygems_version: !ruby/object:Gem::Requirement
105
+ none: false
62
106
  requirements:
63
107
  - - ">="
64
108
  - !ruby/object:Gem::Version
109
+ segments:
110
+ - 0
65
111
  version: "0"
66
- version:
67
112
  requirements: []
68
113
 
69
114
  rubyforge_project:
70
- rubygems_version: 1.3.5
115
+ rubygems_version: 1.3.7
71
116
  signing_key:
72
117
  specification_version: 3
73
- summary: Allows easier database seeding of tables in Rails.
74
- test_files:
75
- - spec/schema.rb
76
- - spec/seed_fu_spec.rb
77
- - spec/spec_helper.rb
118
+ summary: Easily manage seed data in your Active Record application
119
+ test_files: []
120
+
data/README.rdoc DELETED
@@ -1,125 +0,0 @@
1
- = Seed Fu
2
-
3
- Seed Fu is an attempt to once and for all solve the problem of inserting and maintaining seed data in a database. It uses a variety of techniques gathered from various places around the web and combines them to create what is hopefully the most robust seed data system around.
4
-
5
-
6
- == Simple Usage
7
-
8
- Seed data is taken from the <tt>db/fixtures</tt> directory. Simply make descriptive .rb files in that directory (they can be named anything, but users.rb for the User model seed data makes sense, etc.). These scripts will be run whenever the <tt>db:seed</tt> (<tt>db:seed_fu</tt> for Rails 2.3.5 and greater) rake task is called, and in order (you can use <tt>00_first.rb</tt>, <tt>00_second.rb</tt>, etc). You can put arbitrary Ruby code in these files, but remember that it will be executed every time the rake task is called, so it needs to be runnable multiple times on the same database.
9
-
10
- You can also have environment-specific seed data placed in <tt>db/fixtures/ENVIRONMENT</tt> that will only be loaded if that is the current environment.
11
-
12
- Let's say we want to populate a few default users. In <tt>db/fixtures/users.rb</tt> we write the following code:
13
-
14
- User.seed(:login, :email) do |s|
15
- s.login = "bob"
16
- s.email = "bob@bobson.com"
17
- s.first_name = "Bob"
18
- s.last_name = "Bobson"
19
- end
20
-
21
- User.seed(:login, :email) do |s|
22
- s.login = "bob"
23
- s.email = "bob@stevenson.com"
24
- s.first_name = "Bob"
25
- s.last_name = "Stevenson"
26
- end
27
-
28
- That's all you have to do! You will now have two users created in the system and you can change their first and last names in the users.rb file and it will be updated as such.
29
-
30
- The arguments passed to the <tt><ActiveRecord>.seed</tt> method are the constraining attributes: these must remain unchanged between db:seed calls to avoid data duplication. By default, seed data will constrain to the id like so:
31
-
32
- Category.seed do |s|
33
- s.id = 1
34
- s.name = "Buttons"
35
- end
36
-
37
- Category.seed do |s|
38
- s.id = 2
39
- s.name = "Bobbins"
40
- s.parent_id = 1
41
- end
42
-
43
- Note that any constraints that are passed in must be present in the subsequent seed data setting.
44
-
45
- == Seed-many Usage
46
-
47
- The default <tt>.seed` syntax is very verbose. An alternative is the `.seed_many</tt> syntax. Look at this refactoring of the first seed usage example above:
48
-
49
- User.seed_many(:login, :email, [
50
- { :login => "bob", :email => "bob@bobson.com", :first_name => "Bob", :last_name = "Bobson" },
51
- { :login => "bob", :email => "bob@stevenson.com", :first_name => "Bob", :last_name = "Stevenson" }
52
- ])
53
-
54
- Not as pretty, but much more concise.
55
-
56
- == Handling Large SeedFu Files
57
-
58
- Seed files can be huge. To handle large files (over a million rows), try these tricks:
59
-
60
- * Gzip your fixtures. Seed Fu will read .rb.gz files happily. <tt>gzip -9</tt> gives the best compression, and with Seed Fu's repetitive syntax, a 160M file can shrink to 16M.
61
- * Add lines reading <tt># BREAK EVAL</tt> in your big fixtures, and Seed Fu will avoid loading the whole file into memory. If you use SeedFu::Writer, these breaks are built into your generated fixtures.
62
- * Load a single fixture with the <tt>SEED</tt> environment variable: <tt>SEED=cities,states rake db:seed > seed_log`. Each argument to `SEED</tt> is used as a regex to filter fixtures by filename.
63
-
64
- == Generating SeedFu Files
65
-
66
- Say you have a CSV you need to massage and store as seed files. You can create an import script using SeedFu::Writer.
67
-
68
- #!/usr/bin/env ruby
69
- #
70
- # This is: script/generate_cities_seed_from_csv
71
- #
72
- require 'rubygems'
73
- require 'fastercsv'
74
- require File.join( File.dirname(__FILE__), '..', 'vendor/plugins/seed-fu/lib/seed-fu/writer' )
75
-
76
- # Maybe SEEF_FILE could be $stdout, hm.
77
- #
78
- CITY_CSV = File.join( File.dirname(__FILE__), '..', 'city.csv' )
79
- SEED_FILE = File.join( File.dirname(__FILE__), '..', 'db', 'fixtures', 'cities.rb' )
80
-
81
- # Create a seed_writer, walk the CSV, add to the file.
82
- #
83
-
84
- seed_writer = SeedFu::Writer::SeedMany.new(
85
- :seed_file => SEED_FILE,
86
- :seed_model => 'City',
87
- :seed_by => [ :city, :state ]
88
- )
89
-
90
- FasterCSV.foreach( CITY_CSV,
91
- :return_headers => false,
92
- :headers => :first_row
93
- ) do |row|
94
-
95
- # Skip all but the US
96
- #
97
- next unless row['country_code'] == 'US'
98
-
99
- unless us_state
100
- puts "No State Match for #{row['region_name']}"
101
- next
102
- end
103
-
104
- # Write the seed
105
- #
106
- seed_writer.add_seed({
107
- :zip => row['zipcode'],
108
- :state => row['state'],
109
- :city => row['city'],
110
- :latitude => row['latitude'],
111
- :longitude => row['longitude']
112
- })
113
-
114
- end
115
-
116
- seed_writer.finish
117
-
118
- There is also a SeedFu::Writer::Seed in case you prefere the seed()
119
- syntax over the seen_many() syntax. Easy-peasy.
120
-
121
- == Contributors
122
-
123
- * Thanks to Matthew Beale for his great work in adding the writer, making it faster and better.
124
-
125
- Copyright (c) 2008-2009 Michael Bleigh released under the MIT license