blythedunham-ar_dumper 1.2.0.1

Sign up to get free protection for your applications and to get access to all the features.
Files changed (37) hide show
  1. data/LICENSE +20 -0
  2. data/README.rdoc +77 -0
  3. data/Rakefile +76 -0
  4. data/db/migrate/generic_schema.rb +31 -0
  5. data/init.rb +3 -0
  6. data/lib/ar_dumper.rb +5 -0
  7. data/lib/ar_dumper_active_record.rb +31 -0
  8. data/lib/ar_dumper_base.rb +505 -0
  9. data/lib/ar_dumper_controller.rb +52 -0
  10. data/lib/xml_serializer_dumper_support.rb +49 -0
  11. data/test/ar_dumper_controller_test.rb +10 -0
  12. data/test/ar_dumper_test.rb +101 -0
  13. data/test/data/expected_results/all_books.csv +10 -0
  14. data/test/data/expected_results/all_books.xml +93 -0
  15. data/test/data/expected_results/all_books.yml +90 -0
  16. data/test/data/expected_results/except_title_and_author.csv +10 -0
  17. data/test/data/expected_results/except_title_and_author.xml +75 -0
  18. data/test/data/expected_results/except_title_and_author.yml +72 -0
  19. data/test/data/expected_results/only_title_and_author.csv +10 -0
  20. data/test/data/expected_results/only_title_and_author.xml +39 -0
  21. data/test/data/expected_results/only_title_and_author.yml +36 -0
  22. data/test/data/expected_results/proc.csv +3 -0
  23. data/test/data/expected_results/proc.xml +11 -0
  24. data/test/data/expected_results/proc.yml +8 -0
  25. data/test/data/expected_results/second_book.csv +2 -0
  26. data/test/data/expected_results/second_book.xml +13 -0
  27. data/test/data/expected_results/second_book.yml +10 -0
  28. data/test/data/expected_results/topic_name.csv +10 -0
  29. data/test/data/expected_results/topic_name.xml +48 -0
  30. data/test/data/expected_results/topic_name.yml +45 -0
  31. data/test/fixtures/books.yml +65 -0
  32. data/test/fixtures/topics.yml +5 -0
  33. data/test/models/book.rb +8 -0
  34. data/test/models/topic.rb +13 -0
  35. data/test/run.rb +38 -0
  36. data/test/test_helper.rb +60 -0
  37. metadata +96 -0
data/LICENSE ADDED
@@ -0,0 +1,20 @@
1
+ Copyright (c) 2007 Blythe Dunham
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.
@@ -0,0 +1,77 @@
1
+ = ActiveRecord Dumper
2
+ Formats ActiveRecord data in chunks and dumps it to a file, temporary file, or string. Specify the page_size used to paginate records and flush files.
3
+
4
+ == Specify Output
5
+ * <tt>:filename</tt> the name of the file to create. By default will create a file based on the timestamp.
6
+ * <tt>:file_extension</tt> appends file extension unless one exists in file name
7
+ * <tt>:only</tt> a list of the attributes to be included. By default, all column_names are used.
8
+ * <tt>:except</tt> a list of the attributes to be excluded. By default, all column_names are used. This option is not available if +:only+ is used
9
+ * <tt>:methods</tt> a list of the methods to be called on the object
10
+ * <tt>:procs</tt> hash of header name to Proc object
11
+
12
+ === Attributes (Only and Exclude)
13
+ Specify which attributes to include and exclude
14
+ Book.dumper :yml, :only => [:author_name, :title]
15
+
16
+ Book.dump :csv, :except => [:topic_id]
17
+
18
+ === Methods
19
+ Use <tt>:methods</tt> to include methods on the record that are not column attributes
20
+ BigOle.dumper :csv, :methods => [:age, :favorite_food]
21
+ Output..
22
+ ..other attributes.., 25, doughnuts
23
+
24
+ === Proc
25
+ To call procs on the object(s) use <tt>:procs</tt> with a hash of name to value
26
+ The dumper options hash are provided to the proc, and contains the current record <tt>options[:record]</tt> is provided
27
+
28
+ ==== Proc Options Hash
29
+ * <tt>:record</tt> - the active record
30
+ * <tt>:result_set</tt> - the current result set
31
+ * <tt>:counter</tt> - the number of the record
32
+ * <tt>:page_num</tt> - the page number
33
+ * <tt>:target</tt> - the file/string target
34
+
35
+ topic_content_proc = Proc.new{|options| options[:record].topic ? options[:record].topic.content : 'NO CONTENT' }
36
+ Book.dumper :procs => {:topic_content => topic_content_proc}})
37
+
38
+ <book>
39
+ # ... other attributes and methods ...
40
+ <topic-content>NO CONTENT</my_rating>
41
+ </book>
42
+
43
+ == Finder Methods
44
+ * <tt>:find</tt> a map of the finder options passed to find. For example, <tt> {:conditions => ['hairy = ?', 'of course'], :include => :rodents}</tt>
45
+ * <tt>:records</tt> - the records to be dumped instead of using a find
46
+
47
+ == Format Header
48
+ * <tt>:header</tt> when a hash is specified, maps the field name to the header name. For example <tt>{:a => 'COL A', :b => 'COL B'}</tt> would print 'COL A', 'COL B'
49
+ when an array is specified uses this instead of the fields
50
+ when true or by default prints the fields
51
+ when false does not include a header
52
+ * <tt>:text_format</tt> a string method such as <tt>:titleize</tt>, <tt>:dasherize</tt>, <tt>:underscore</tt> to format the on all the headers. If an attribute is <tt>:email_address</tt> and <tt>:titleize</tt> is chosen, then the Header value is "Email Address"
53
+ * <tt>:root</tt> In xml, this is the name of the highest level list object. The plural of the class name is the default. For yml, this is the base name of the
54
+ the objects. Each record will be root_id. For example, contact_2348
55
+
56
+ == Filename and Target
57
+ <tt>:target_type</tt> The target_type for the data. Defaults to <tt>:file</tt>.
58
+ * <tt>:string</tt> prints to string. Do not use with large data sets
59
+ * <tt>:tmp_file</tt>. Use a temporary file that is destroyed when the process exists
60
+ * <tt>:file</tt>. Use a standard file
61
+ <tt>:filename</tt> basename of the file. Defaults to random time based string for non-temporary files
62
+ <tt>:file_extension</tt> Extension (suffix) like .csv, .xml. Added only if the basename has no suffix.
63
+ <tt>:file_extension</tt> is only available when <tt>:target_type_type => :file</tt>
64
+ <tt>:file_path</tt> path or directory of the file. Defaults to dumper_file_path or temporary directories
65
+
66
+ == Format specific options
67
+ * <tt>:csv</tt> - any options to pass to csv parser. Example <tt> :csv => { :col_sep => "\t" }</tt>
68
+ * <tt>:xml</tt> - any options to pass to xml parser. Example <tt> :xml => { :indent => 4 } </tt>
69
+
70
+ === Installation
71
+ script/plugin install git://github.com/blythedunham/ar_dumper.git
72
+ === Developers
73
+ Blythe Dunham http://snowgiraffe.com
74
+
75
+ === Homepage
76
+ * Project Site: http://github.com/blythedunham/ar_dumper/tree/master
77
+ * Rdoc: http://snowgiraffe.com/rdocs/ar_dumper
@@ -0,0 +1,76 @@
1
+
2
+ require 'rubygems'
3
+ require 'rake/gempackagetask'
4
+ require 'rake/testtask'
5
+ require 'rake/rdoctask'
6
+
7
+ desc 'Default: run unit tests.'
8
+ task :default => [:clean, :test]
9
+
10
+ desc 'Clean up files.'
11
+ task :clean do |t|
12
+ FileUtils.rm_rf "doc"
13
+ FileUtils.rm_rf "tmp"
14
+ FileUtils.rm_rf "pkg"
15
+ FileUtils.rm "debug.log" rescue nil
16
+ FileUtils.rm "test/debug.log" rescue nil
17
+ Dir.glob("ar_dumper-*.gem").each{|f| FileUtils.rm f }
18
+ end
19
+
20
+ include_file_globs = ["README*",
21
+ "LICENSE",
22
+ "Rakefile",
23
+ "init.rb",
24
+ "{db,lib,test}/**/*"]
25
+
26
+ spec = Gem::Specification.new do |s|
27
+ s.name = "ar_dumper"
28
+ s.version = "1.2.0.1"
29
+ s.author = "Blythe Dunham"
30
+ s.email = "blythe@snowgiraffe.com"
31
+ s.homepage = "http://github.com/blythedunham/ar_dumper"
32
+ s.platform = Gem::Platform::RUBY
33
+ s.summary = "Extends ActiveRecord to export volume records to a file, string, or temp file in csv, yaml, or xml formats"
34
+ s.files = FileList[include_file_globs].to_a
35
+ s.require_path = "lib"
36
+ s.test_files = FileList["test/**/*_test.rb"].to_a
37
+ s.rubyforge_project = "ar_dumper"
38
+ s.has_rdoc = true
39
+ s.extra_rdoc_files = FileList["README*"].to_a
40
+ s.rdoc_options << '--line-numbers' << '--inline-source'
41
+ end
42
+
43
+ desc "Print a list of the files to be put into the gem"
44
+ task :manifest => :clean do
45
+ spec.files.each do |file|
46
+ puts file
47
+ end
48
+ end
49
+
50
+ desc "Generate a gemspec file for GitHub"
51
+ task :gemspec => :clean do
52
+ File.open("#{spec.name}.gemspec", 'w') do |f|
53
+ f.write spec.to_ruby
54
+ end
55
+ end
56
+
57
+ desc "Build the gem into the current directory"
58
+ task :gem => :gemspec do
59
+ `gem build #{spec.name}.gemspec`
60
+ end
61
+
62
+
63
+ Rake::GemPackageTask.new(spec) do |pkg|
64
+ pkg.need_tar = true
65
+ end
66
+
67
+ desc "Run tests"
68
+ Rake::TestTask.new("test") { |t|
69
+ t.pattern = 'test/*_test.rb'
70
+ t.verbose = true
71
+ t.warning = true
72
+ }
73
+
74
+ task :default => "pkg/#{spec.name}-#{spec.version}.gem" do
75
+ puts "generated latest version"
76
+ end
@@ -0,0 +1,31 @@
1
+ ActiveRecord::Schema.define do
2
+
3
+
4
+ create_table :topics, :force=>true do |t|
5
+ t.column :title, :string, :null=>false
6
+ t.column :author_name, :string
7
+ t.column :author_email_address, :string
8
+ t.column :written_on, :datetime
9
+ t.column :bonus_time, :time
10
+ t.column :last_read, :datetime
11
+ t.column :content, :text
12
+ t.column :approved, :boolean, :default=>'1'
13
+ t.column :replies_count, :integer
14
+ t.column :parent_id, :integer
15
+ t.column :type, :string
16
+ t.column :created_at, :datetime
17
+ t.column :updated_at, :datetime
18
+ end
19
+
20
+
21
+ create_table :books, :force=>true do |t|
22
+ t.column :title, :string, :null=>false
23
+ t.column :publisher, :string, :null=>false, :default => 'Default Publisher'
24
+ t.column :author_name, :string, :null=>false
25
+ t.column :created_at, :datetime
26
+ t.column :updated_at, :datetime
27
+ t.column :topic_id, :integer
28
+ t.column :for_sale, :boolean, :default => true
29
+ end
30
+
31
+ end
data/init.rb ADDED
@@ -0,0 +1,3 @@
1
+ require File.dirname(__FILE__) + '/lib/ar_dumper'
2
+
3
+
@@ -0,0 +1,5 @@
1
+ require 'csv'
2
+ require File.dirname(__FILE__) + '/xml_serializer_dumper_support'
3
+ require File.dirname(__FILE__) + '/ar_dumper_base'
4
+ require File.dirname(__FILE__) + '/ar_dumper_active_record'
5
+ require File.dirname(__FILE__) + '/ar_dumper_controller'
@@ -0,0 +1,31 @@
1
+ # Refer to ArDumper for ActiveRecord extensions
2
+ class ActiveRecord::Base
3
+
4
+ # Dump to csv string
5
+ # Same as <tt>ActiveRecord::Base.dumper :csv, :target_type => :string</tt>
6
+ def self.to_csv(options={})
7
+ dump_to_string :csv, options
8
+ end
9
+
10
+ # Dump to yml
11
+ # Same as <tt>ActiveRecord::Base.dumper :yml</tt>
12
+ def self.dump_to_yaml(options={})
13
+ dumper :yml, options
14
+ end
15
+
16
+ # Dump data
17
+ # * +format+ - type of dump (<tt>:csv</tt>, <tt>:yml</tt>, <tt>:xml</tt>)
18
+ # * +options+ - Dumper options. Refer to ArDumper
19
+ def self.dumper(format, options={})
20
+ ArDumper.new(self, options).dump(format)
21
+ end
22
+
23
+ # Dump to string.
24
+ # Same as <tt> ActiveRecord::Base.dumper format, :target_type => :string</tt>
25
+ def self.dumper_to_string(format, options={})
26
+ dumper(format, options.update({:target_type => :string}))
27
+ end
28
+
29
+ end
30
+
31
+
@@ -0,0 +1,505 @@
1
+ #############################################################################
2
+ #
3
+ # Formats ActiveRecord data in chunks and dumps it to a file, temporary file, or string. Specify the page_size used to paginate
4
+ # records and flush files.
5
+ #
6
+ # ==Specify Output
7
+ # * <tt>:filename</tt> the name of the file to create. By default will create a file based on the timestamp.
8
+ # * <tt>:file_extension</tt> appends file extension unless one exists in file name
9
+ # * <tt>:only</tt> a list of the attributes to be included. By default, all column_names are used.
10
+ # * <tt>:except</tt> a list of the attributes to be excluded. By default, all column_names are used. This option is not available if +:only+ is used
11
+ # * <tt>:methods</tt> a list of the methods to be called on the object
12
+ # * <tt>:procs</tt> hash of header name to Proc object
13
+ #
14
+ # === Attributes (Only and Exclude)
15
+ # Specify which attributes to include and exclude
16
+ # Book.dumper :yml, :only => [:author_name, :title]
17
+ #
18
+ # Book.dump :csv, :except => [:topic_id]
19
+ #
20
+ # === Methods
21
+ # Use <tt>:methods</tt> to include methods on the record that are not column attributes
22
+ # BigOle.dumper :csv, :methods => [:age, :favorite_food]
23
+ # Output..
24
+ # ..other attributes.., 25, doughnuts
25
+ #
26
+ # === Proc
27
+ # To call procs on the object(s) use <tt>:procs</tt> with a hash of name to value
28
+ # The dumper options hash are provided to the proc, and contains the current record <tt>options[:record]</tt> is provided
29
+ #
30
+ # ==== Proc Options Hash
31
+ # <tt>:record</tt> - the active record
32
+ # <tt>:result_set</tt> - the current result set
33
+ # <tt>:counter</tt> - the number of the record
34
+ # <tt>:page_num</tt> - the page number
35
+ # <tt>:target</tt> - the file/string target
36
+ #
37
+ # topic_content_proc = Proc.new{|options| options[:record].topic ? options[:record].topic.content : 'NO CONTENT' }
38
+ # Book.dumper :procs => {:topic_content => topic_content_proc}})
39
+ #
40
+ # <book>
41
+ # # ... other attributes and methods ...
42
+ # <topic-content>NO CONTENT</my_rating>
43
+ # </book>
44
+ #
45
+ # == Finder Methods
46
+ # * <tt>:find</tt> a map of the finder options passed to find. For example, <tt> {:conditions => ['hairy = ?', 'of course'], :include => :rodents}</tt>
47
+ # * <tt>:records</tt> - the records to be dumped instead of using a find
48
+ #
49
+ # == Format Header
50
+ # * <tt>:header</tt> when a hash is specified, maps the field name to the header name. For example <tt>{:a => 'COL A', :b => 'COL B'}</tt> would print 'COL A', 'COL B'
51
+ # when an array is specified uses this instead of the fields
52
+ # when true or by default prints the fields
53
+ # when false does not include a header
54
+ # * <tt>:text_format</tt> a string method such as <tt>:titleize</tt>, <tt>:dasherize</tt>, <tt>:underscore</tt> to format the on all the headers. If an attribute is <tt>:email_address</tt> and <tt>:titleize</tt> is chosen, then the Header value is "Email Address"
55
+ # * <tt>:root</tt> In xml, this is the name of the highest level list object. The plural of the class name is the default. For yml, this is the base name of the
56
+ # the objects. Each record will be root_id. For example, contact_2348
57
+ #
58
+ # == Filename and Target
59
+ # <tt>:target_type</tt> The target_type for the data. Defaults to <tt>:file</tt>.
60
+ # * <tt>:string</tt> prints to string. Do not use with large data sets
61
+ # * <tt>:tmp_file</tt>. Use a temporary file that is destroyed when the process exists
62
+ # * <tt>:file</tt>. Use a standard file
63
+ # <tt>:filename</tt> basename of the file. Defaults to random time based string for non-temporary files
64
+ # <tt>:file_extension</tt> Extension (suffix) like .csv, .xml. Added only if the basename has no suffix.
65
+ # <tt>:file_extension</tt> is only available when <tt>:target_type_type => :file</tt>
66
+ # <tt>:file_path</tt> path or directory of the file. Defaults to dumper_file_path or temporary directories
67
+ #
68
+ # == Format specific options
69
+ # * <tt>:csv</tt> - any options to pass to csv parser. Example <tt> :csv => { :col_sep => "\t" }</tt>
70
+ # * <tt>:xml</tt> - any options to pass to xml parser. Example <tt> :xml => { :indent => 4 } </tt>
71
+ #
72
+ # === Installation
73
+ # script/plugin install git://github.com/blythedunham/ar_dumper.git
74
+ # === Developers
75
+ # Blythe Dunham http://snowgiraffe.com
76
+ #
77
+ # === Homepage
78
+ # * Project Site: http://github.com/blythedunham/ar_dumper/tree/master
79
+ # * Rdoc: http://snowgiraffe.com/rdocs/ar_dumper
80
+ #
81
+ class ArDumper
82
+
83
+ #Page Size. Default 50
84
+ cattr_accessor :dumper_page_size
85
+ @@dumper_page_size ||= 50
86
+
87
+ # File Directory where dumps are stored
88
+ # Defaults to temporary directory
89
+ cattr_accessor :dumper_file_path
90
+ @@dumper_file_path ||= ENV['TMPDIR']||ENV['TMP']||ENV['TEMP']||'.'
91
+
92
+ # Basename of the dump files, defaulted to ardumper
93
+ # Ex. ardumper.book.2348723947.23423.xml
94
+ cattr_accessor :dumper_tmp_file_basename #default basename of temporary files
95
+ @@dumper_tmp_file_basename ||= 'ardumper'
96
+
97
+ # Specify the csv writer. Defaults to faster csv if available
98
+ cattr_accessor :csv_writer
99
+
100
+ attr_reader :fields
101
+ attr_reader :klass
102
+ attr_reader :options
103
+
104
+ def initialize(klass, dump_options={})#:nodoc:
105
+ @klass = klass
106
+ @options = dump_options
107
+ build_attribute_list
108
+
109
+ unless options[:text_format].nil? || String.new.respond_to?(options[:text_format])
110
+ raise ArDumperException.new("Invalid value for option :text_format #{options[:text_format]}")
111
+ end
112
+ end
113
+
114
+ # build a list of attributes, methods and procs
115
+ def build_attribute_list#:nodoc:
116
+ if options[:only]
117
+ options[:attributes] = Array(options[:only])
118
+ else
119
+ options[:attributes] = @klass.column_names - Array(options[:except]).collect { |e| e.to_s }
120
+ end
121
+
122
+ options[:attributes] = options[:attributes].collect{|attr| "#{attr}"}
123
+ options[:methods] = options[:methods].is_a?(Hash) ? options[:methods].values : Array(options[:methods])
124
+
125
+ #if procs are specified as an array separate the headers(keys) from the procs(values)
126
+ if options[:procs].is_a?(Hash)
127
+ options[:proc_headers]= options[:procs].keys
128
+ options[:procs]= options[:procs].values
129
+ else
130
+ options[:procs] = Array(options[:procs])
131
+ options[:proc_headers]||= Array.new
132
+ 0.upto(options[:procs].size - options[:proc_headers].size - 1) {|idx| options[:proc_headers] << "proc_#{idx}" }
133
+ end
134
+
135
+ end
136
+
137
+ # Dump to the appropriate format
138
+ def dump(format)#:nodoc:
139
+
140
+ case format.to_sym
141
+ when :csv
142
+ dump_to_csv
143
+
144
+ when :xml
145
+ dump_to_xml
146
+
147
+ when :yaml, :fixture, :yml
148
+ dump_to_fixture
149
+
150
+ else
151
+ raise ArDumperException.new("Unknown format #{format}. Please specify :csv, :xml, or :yml ")
152
+ end
153
+ end
154
+
155
+ # Wrapper around the dump. The main dump functionality
156
+ def dumper(file_extension=nil, header = nil, footer = nil, &block)#:nodoc:
157
+
158
+ options[:counter] = -1
159
+ begin
160
+ #get the file parameters
161
+ target = prepare_target(file_extension)
162
+ target << header if header
163
+ ArDumper.paginate_dump_records(@klass, @options) do |records, page_num|
164
+
165
+ #save state on options to make it accessible by
166
+ #class and procs
167
+ options[:result_set] = records
168
+ options[:page_num] = page_num
169
+
170
+ records.each do |record|
171
+ options[:record] = record
172
+ yield record
173
+ end
174
+
175
+ #flush after each set
176
+ target.flush if target.respond_to?(:flush)
177
+ end
178
+ target << footer if footer
179
+
180
+ #final step close the options[:target]
181
+ ensure
182
+ target.close if target && target.respond_to?(:close)
183
+ end
184
+
185
+ options[:full_file_name]||target
186
+ end
187
+
188
+ #collect the record data into an array
189
+ def dump_record(record)#:nodoc:
190
+ record_values = @options[:attributes].inject([]){|values, attr| values << record["#{attr}"]; values }
191
+ record_values = @options[:methods].inject(record_values) {|values, method| values << record.send(method); values }
192
+ record_values = @options[:procs].inject(record_values){|values, proc| values << proc.call(options); values }
193
+ record_values
194
+ end
195
+
196
+
197
+ #############################################################################
198
+ # XML Dumper
199
+ #############################################################################
200
+ #
201
+ # Dumps the data to an xml file
202
+ #
203
+ # Using the ActiveRecord version of dumper so we CANNOT specify fields that are not attributes
204
+ #
205
+ # In addition to options listed in +dumper+:
206
+ # <tt>:xml</tt> - xml options for the xml_serializer. Includes <tt>:indent</tt>, <tt>:skip_instruct</tt>, <tt>:margin</tt>
207
+ #
208
+ # Note that <tt>:procs</tt> will use the dumper proc and pass the dumper options
209
+ # Book.dump :xml, :procs => {:topic_content => Proc.new { |options| options[:record].topic.content }}
210
+ # To use xml proc, specify <tt> :xml => {:procs => array_of_procs}</tt>
211
+ # Book.dump :xml, :xml => {:procs => [Proc.new{|xml_options| xml_options[:builder].tag 'abc', 'def'}]}
212
+ def dump_to_xml#:nodoc:
213
+
214
+ #preserve the original skip instruct
215
+ skip_instruct = @options[:xml] && @options[:xml][:skip_instruct].is_a?(TrueClass)
216
+
217
+ self.options[:procs]||= []
218
+
219
+ #use the fields if :only is not specified in the xml options
220
+ xml_options = {
221
+ :only => @options[:only],
222
+ :except => @options[:except],
223
+ :methods => @options[:methods]
224
+ }
225
+
226
+ xml_options.update(@options[:xml]) if @options[:xml]
227
+
228
+ #do not instruct for each set
229
+ xml_options[:skip_instruct] = true
230
+ xml_options[:indent]||=2
231
+ xml_options[:margin] = xml_options[:margin].to_i + 1
232
+
233
+ #set the variable on the options
234
+ options[:xml] = xml_options
235
+
236
+ #builder for header and footer
237
+ builder_options = {
238
+ :margin => xml_options[:margin] - 1,
239
+ :indent => xml_options[:indent]
240
+ }
241
+
242
+ options[:root] = (options[:root] || @klass.to_s.underscore.pluralize).to_s
243
+
244
+ #use the builder to make sure we are indented properly
245
+ builder = Builder::XmlMarkup.new(builder_options.clone)
246
+ builder.instruct! unless skip_instruct
247
+ builder << "<#{options[:root]}>\n"
248
+ header = builder.target!
249
+
250
+ #get the footer. Using the builder will make sure we are indented properly
251
+ builder = Builder::XmlMarkup.new(builder_options)
252
+ builder << "</#{options[:root]}>"
253
+ footer = builder.target!
254
+
255
+ dumper(:xml, header, footer) do |record|
256
+ options[:target] << serialize_record_dump_xml(record, xml_options)
257
+ end
258
+ end
259
+
260
+ # Serialize the xml data for the given record
261
+ def serialize_record_dump_xml(record, xml_options)#:nodoc:
262
+
263
+ serializer = ActiveRecord::XmlSerializer.new(record, xml_options.dup)
264
+ xml = serializer.to_s do |builder|
265
+ self.options[:procs].each_with_index do |proc, idx|
266
+ serializer.add_tag_for_value(self.options[:proc_headers][idx].to_s,
267
+ proc.call(self.options))
268
+ end
269
+ end
270
+ end
271
+
272
+
273
+ #############################################################################
274
+ # Yaml/Fixture Dumper
275
+ #############################################################################
276
+ # dumps the data to a fixture file
277
+ # In addition to options listed in +dumper+:
278
+ # <tt>:root</tt> Basename of the record. Defaults to the class name so each record is named customer_1
279
+ def dump_to_fixture#:nodoc:
280
+ basename = @options[:root]||@klass.table_name.singularize
281
+ header_list = build_header_list
282
+
283
+ # doctor the yaml a bit to print the hash header at the top
284
+ # instead of each record
285
+ dumper(:yml, "---\s") do |record|
286
+ record_data = Hash.new
287
+ dump_record(record).each_with_index{|field, idx| record_data[header_list[idx].to_s] = field.to_s }
288
+ options[:target] << {"#{basename}_#{record.id}" => record_data}.to_yaml.gsub(/^---\s\n/, "\n")
289
+ end
290
+ end
291
+
292
+
293
+
294
+ #############################################################################
295
+ # CSV DUMPER
296
+ #############################################################################
297
+ #
298
+ # Dump csv data
299
+ #
300
+ # * <tt>:csv</tt> - any options to pass to csv parser.
301
+ # :col_sep Example + :csv => {:col_sep => "\t"} +
302
+ # :row_sep Row seperator
303
+ # * <tt>:page_size</tt> - the page size to use. Defaults to dumper_page_size or 50
304
+ def dump_to_csv#:nodoc:
305
+ header = nil
306
+ @options[:csv]||={}
307
+
308
+ if !@options[:header].is_a?(FalseClass)
309
+ header_list = build_header_list
310
+ #print the header unless set to false
311
+ header = write_csv_row(header_list)
312
+ end
313
+
314
+ dumper(:csv, header) do |record|
315
+ options[:target] << write_csv_row(dump_record(record))
316
+ end
317
+ end
318
+
319
+ # Write out the csv row using the selected csv writer
320
+ def write_csv_row(row_data, header_list=[])#:nodoc:
321
+ if csv_writer == :faster
322
+ ::FasterCSV::Row.new(header_list, row_data).to_csv(@options[:csv])
323
+ else
324
+ ::CSV.generate_line(row_data, @options[:csv][:col_sep], @options[:csv][:row_sep]) + (@options[:csv][:row_sep]||"\n")
325
+ end
326
+ end
327
+
328
+ #Try to use the FasterCSV if it exists
329
+ #otherwise use csv
330
+ def csv_writer #:nodoc:
331
+ unless @@csv_writer
332
+ @@csv_writer = :faster
333
+ begin
334
+ require 'faster_csv'#:nodoc:
335
+ ::FasterCSV
336
+ rescue Exception => exc
337
+ @@csv_writer = :normal
338
+ end
339
+ end
340
+ @@csv_writer
341
+ end
342
+
343
+ # Returns an array with the header names
344
+ # This will be in the same order as the data returned by dump_record
345
+ # attributes + methods + procs
346
+ #
347
+ # <tt>:header</tt> The header defaults to the attributes and method names. When set
348
+ # to false no header is specified
349
+ # * +hash+ A map from attribute or method name to Header column name
350
+ # * +array+ A list in the same order that is used to display record data
351
+ #
352
+ # <tt>:procs</tt> If a hash, then the keys are the names. If an array, then use proc_1, proc_2, etc
353
+ # <tt>:text_format</tt> Format names with a text format such as +:titlieze+, +:dasherize+, +:underscore+
354
+ def build_header_list#:nodoc:
355
+
356
+ header_options = options[:header]
357
+ columns = @options[:attributes] + @options[:methods]
358
+ header_names =
359
+ if header_options.is_a?(Hash)
360
+ header_options.symbolize_keys!
361
+
362
+ #Get the header for each attribute and method
363
+ columns.collect{|field|(header_options[field.to_sym]||field).to_s}
364
+
365
+ #ordered by attributes, methods, then procs
366
+ elsif header_options.is_a?(Array)
367
+ header_names = header_options
368
+ header_names.concat(columns[header_options.length..-1]) if header_names.length < columns.length
369
+
370
+ #default to column names
371
+ else
372
+ columns
373
+ end
374
+
375
+ #add process names
376
+ header_names.concat(options[:proc_headers])
377
+
378
+ #format names with a text format such as titlieze, dasherize, underscore
379
+ header_names.collect!{|n| n.to_s.send(options[:text_format])} if options[:text_format]
380
+
381
+ header_names
382
+ end
383
+
384
+
385
+
386
+
387
+
388
+ # Create the options[:target](file) based on these options. The options[:target] must respond to <<
389
+ # Current options[:target]s are :string, :file, :tempfile
390
+ #
391
+ # <tt> :target_type</tt> The options[:target] for the data. Defaults to +:file+
392
+ # * :string prints to string. Do not use with large data sets
393
+ # * :tmp_file. Use a temporary file that is destroyed when the process exists
394
+ # * :file. Use a standard file
395
+ # <tt> :filename </tt> basename of the file. Defaults to random time based string for non-temporary files
396
+ # <tt> :file_extension </tt> Extension (suffix) like .csv, .xml. Added only if the basename has no suffix.
397
+ # :file_extension is only available when +:target_type_type => :file+
398
+ # <tt> :file_path </tt> path or directory of the file. Defaults to dumper_file_path or temporary directories
399
+
400
+ def prepare_target(file_extension = nil)#:nodoc:
401
+
402
+ options[:target] = case options[:target_type]
403
+ #to string option dumps to a string instead of a file
404
+ when :string
405
+ String.new
406
+
407
+ #use a temporary file
408
+ #open a temporary file with the basename specified by filename
409
+ #defaults to the value of one of the environment variables TMPDIR, TMP, or TEMP
410
+ when :tmp_file
411
+
412
+ Tempfile.open(options[:filename]||(@@dumper_tmp_file_basename+@klass.name.downcase),
413
+ options[:file_path]||@@dumper_file_path)
414
+
415
+ #default to a real file
416
+ else
417
+ extension = options[:file_extension]||file_extension
418
+ mode = options[:append_to_file].is_a?(TrueClass)? 'a' : 'w'
419
+ filename = options[:filename]||"#{@@dumper_tmp_file_basename}.#{@klass.name.downcase}.#{Time.now.to_f.to_s}.#{extension}"
420
+
421
+ #append an extension unless one already exists
422
+ filename += ".#{extension}" if extension && !filename =~ /\.\w*$/
423
+
424
+ #get the file path if the filename does not contain one
425
+ if File.basename(filename) == filename
426
+ path = options[:file_path]||@@dumper_file_path
427
+ filename = File.join(path, filename) unless path.blank?
428
+ end
429
+
430
+
431
+ File.open(filename, mode)
432
+ end
433
+
434
+ options[:full_file_name] = options[:target].path if options[:target].respond_to?(:path)
435
+ options[:target]
436
+ end
437
+
438
+
439
+
440
+ #############################################################################
441
+ # Pagination Helpers (Support before 2.3.2)
442
+ #############################################################################
443
+ #
444
+ #
445
+ # Quick and dirty paginate to loop thru the records page by page
446
+ # Options are:
447
+ # * <tt>:find</tt> - a map of the finder options passed to find. For example, + {:conditions => ['hairy = ?', 'of course'], :include => :rodents} +
448
+ # * <tt>:page_size</tt> - the page size to use. Defaults to dumper_page_size or 50. Set to false to disable pagination
449
+ # * <tt>:records</tt> - the records to be dumped instead of using a find
450
+ def self.paginate_dump_records(klass, options={}, &block)#:nodoc:
451
+ finder_options = (options[:find]||{}).clone
452
+
453
+ if options[:records]
454
+ yield options[:records], 0
455
+ return
456
+ #pagination is not needed when :page_size => false
457
+ elsif options[:page_size].is_a?(FalseClass)
458
+ yield klass.find(:all, finder_options), 0
459
+ return
460
+ end
461
+
462
+ options[:page_size]||= dumper_page_size
463
+
464
+ #limit becomes the maximum amount of records to pull
465
+ max_records = finder_options[:limit]
466
+ page_num = 0
467
+ finder_options[:limit] = compute_page_size(max_records, page_num, options[:page_size])
468
+ records = []
469
+ while (finder_options[:limit] > 0 && (page_num == 0 || records.length == options[:page_size]))
470
+ records = klass.find :all, finder_options.update(:offset => page_num * options[:page_size])
471
+
472
+ yield records, page_num
473
+ page_num = page_num + 1
474
+
475
+ #calculate the limit if an original limit (max_records) was set
476
+ finder_options[:limit] = compute_page_size(max_records, page_num, options[:page_size])
477
+ end
478
+ end
479
+
480
+ def self.compute_page_size(max_records, page_num, page_size)#:nodoc:
481
+ max_records ? [(max_records - (page_num * page_size)), page_size].min : page_size
482
+ end
483
+
484
+ # Quick and dirty paginate to loop thru each page
485
+ # Options are:
486
+ # * <tt>:find</tt> - a map of the finder options passed to find. For example, + {:conditions => ['hairy = ?', 'of course'], :include => :rodents} +
487
+ # * <tt>:page_size</tt> - the page size to use. Defaults to dumper_page_size or 50. Set to false to disable pagination
488
+ def self.paginate_each_record(klass, options={}, &block)#:nodoc:
489
+ counter = -1
490
+ paginate_dump_records(klass, options) do |records, page_num|
491
+ records.each do |record|
492
+ yield record, (counter +=1)
493
+ end
494
+ end
495
+ end
496
+ end
497
+
498
+ class ArDumperException < Exception#:nodoc:
499
+ end
500
+
501
+
502
+
503
+
504
+
505
+