typingpool 0.7.0 → 0.7.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.
Files changed (37) hide show
  1. data/LICENSE +20 -0
  2. data/README.markdown +452 -0
  3. data/lib/typingpool/amazon/hit/assignment/empty.rb +19 -0
  4. data/lib/typingpool/amazon/hit/assignment.rb +43 -0
  5. data/lib/typingpool/amazon/hit/full/fromsearchhits.rb +44 -0
  6. data/lib/typingpool/amazon/hit/full.rb +105 -0
  7. data/lib/typingpool/amazon/hit.rb +458 -0
  8. data/lib/typingpool/amazon/question.rb +45 -0
  9. data/lib/typingpool/amazon.rb +3 -677
  10. data/lib/typingpool/app/cli/formatter.rb +16 -0
  11. data/lib/typingpool/app/cli.rb +64 -0
  12. data/lib/typingpool/app/friendlyexceptions.rb +34 -0
  13. data/lib/typingpool/app.rb +2 -97
  14. data/lib/typingpool/config/root.rb +114 -0
  15. data/lib/typingpool/config.rb +13 -119
  16. data/lib/typingpool/filer/audio.rb +84 -0
  17. data/lib/typingpool/filer/csv.rb +57 -0
  18. data/lib/typingpool/filer/dir.rb +76 -0
  19. data/lib/typingpool/filer/files/audio.rb +63 -0
  20. data/lib/typingpool/filer/files.rb +55 -0
  21. data/lib/typingpool/filer.rb +4 -313
  22. data/lib/typingpool/project/local.rb +117 -0
  23. data/lib/typingpool/project/remote/s3.rb +135 -0
  24. data/lib/typingpool/project/remote/sftp.rb +100 -0
  25. data/lib/typingpool/project/remote.rb +65 -0
  26. data/lib/typingpool/project.rb +2 -396
  27. data/lib/typingpool/template/assignment.rb +17 -0
  28. data/lib/typingpool/template/env.rb +77 -0
  29. data/lib/typingpool/template.rb +2 -87
  30. data/lib/typingpool/test/script.rb +310 -0
  31. data/lib/typingpool/test.rb +1 -306
  32. data/lib/typingpool/transcript/chunk.rb +129 -0
  33. data/lib/typingpool/transcript.rb +1 -125
  34. data/lib/typingpool/utility/castable.rb +65 -0
  35. data/lib/typingpool/utility.rb +1 -61
  36. data/test/test_integration_script_6_tp_finish.rb +1 -0
  37. metadata +135 -81
@@ -0,0 +1,16 @@
1
+ module Typingpool
2
+ module App
3
+ module CLI
4
+ module Formatter
5
+ require 'highline/import'
6
+ def cli_bold(text)
7
+ HighLine.color(text, :bold)
8
+ end
9
+
10
+ def cli_reverse(text)
11
+ HighLine.color(text, :reverse)
12
+ end
13
+ end #Formatter
14
+ end #CLI
15
+ end #App
16
+ end #Typingpool
@@ -0,0 +1,64 @@
1
+ module Typingpool
2
+ module App
3
+ module CLI
4
+ class << self
5
+ include App::FriendlyExceptions
6
+
7
+ #Optionally takes an ostensible path to a config file, as passed
8
+ #as a command-line option. Checks to make sure the file exists;
9
+ #returns nil if does not, returns a Config instance if it
10
+ #does. If no path is passed, the default config file is returned
11
+ #(as retrieved by Config.file with no args).
12
+ def config_from_arg(arg=nil)
13
+ if arg
14
+ path = File.expand_path(arg)
15
+ return unless File.exists?(path) && File.file?(path)
16
+ Config.file(path)
17
+ else
18
+ Config.file
19
+ end #if option
20
+ end
21
+
22
+ #Outputs a friendly explanation of the --help option for
23
+ #appending to script usage banners.
24
+ def help_arg_explanation
25
+ "`#{File.basename($PROGRAM_NAME)} --help` for more information."
26
+ end
27
+
28
+ #Converts a user arg into a Project instance, setting up or
29
+ #consulting a Config along the way.
30
+ # ==== Params
31
+ # [arg] A user-supplied argument specifying either an absolute
32
+ # path to a Project folder (Project#local) or the
33
+ # name of a project folder within
34
+ # [config]#transcripts.
35
+ # [config] A Config instance. If [arg] is an absolute path,
36
+ # will be modified -- Config#itranscripts will be
37
+ # changed to match the implied transcripts dir.
38
+ # ==== Errors
39
+ # Will abort with a friendly message on any errors.
40
+ # ==== Returns
41
+ # A Project instance.
42
+ def project_from_arg_and_config(arg, config)
43
+ path = if (File.exists?(arg) && File.directory?(arg))
44
+ config.transcripts = File.dirname(arg)
45
+ arg
46
+ else
47
+ abort "No 'transcripts' dir specified in your config file and '#{arg}' is not a valid path" unless config.transcripts
48
+ path = File.join(config.transcripts, arg)
49
+ abort "No such project '#{arg}' in dir '#{config.transcripts}'" unless File.exists? path
50
+ abort "'#{arg}' is not a directory at '#{path}'" unless File.directory? path
51
+ path
52
+ end
53
+ project = with_friendly_exceptions('project name', File.basename(path)) do
54
+ Typingpool::Project.new(File.basename(path), config)
55
+ end
56
+ abort "Not a project directory at '#{path}'" unless project.local
57
+ project
58
+ end
59
+
60
+ end #class << self
61
+ require 'typingpool/app/cli/formatter'
62
+ end #CLI
63
+ end #App
64
+ end #Typingpool
@@ -0,0 +1,34 @@
1
+ module Typingpool
2
+ module App
3
+ module FriendlyExceptions
4
+
5
+ #Massages terse exceptions from our model layer into a
6
+ #human-friendly message suitable for an abort message from a
7
+ #command-line script.
8
+ # ==== Params
9
+ # [name] A string used to refer to the input. For example
10
+ # 'project title' or '--config argument'. Used in the
11
+ # goodbye message.
12
+ # [*input] One or more values. The user input that will cause
13
+ # any exceptions. Used in the goodbye message.
14
+ # [&block] The block to execute and monitor for
15
+ # exceptions. Will be passed [*input].
16
+ # ==== Errors
17
+ # Will abort with a friendly message on any exception of the
18
+ # type Typingpool::Error::Argument.
19
+ # ==== Returns
20
+ # The return value of &block.
21
+ def with_friendly_exceptions(name, *input)
22
+ begin
23
+ yield(*input)
24
+ rescue Typingpool::Error::Argument => exception
25
+ goodbye = "Could not make sense of #{name.to_s} "
26
+ goodbye += input.map{|input| "'#{input}'" }.join(', ')
27
+ goodbye += ". #{exception.message}"
28
+ goodbye += '.' unless goodbye.match(/\.$/)
29
+ abort goodbye
30
+ end #begin
31
+ end
32
+ end #FriendlyExceptions
33
+ end #App
34
+ end #Typingpool
@@ -533,102 +533,7 @@ module Typingpool
533
533
  end #assignments.each!...
534
534
  end
535
535
  end #class << self
536
- module FriendlyExceptions
537
- #Massages terse exceptions from our model layer into a
538
- #human-friendly message suitable for an abort message from a
539
- #command-line script.
540
- # ==== Params
541
- # [name] A string used to refer to the input. For example
542
- # 'project title' or '--config argument'. Used in the
543
- # goodbye message.
544
- # [*input] One or more values. The user input that will cause
545
- # any exceptions. Used in the goodbye message.
546
- # [&block] The block to execute and monitor for
547
- # exceptions. Will be passed [*input].
548
- # ==== Errors
549
- # Will abort with a friendly message on any exception of the
550
- # type Typingpool::Error::Argument.
551
- # ==== Returns
552
- # The return value of &block.
553
- def with_friendly_exceptions(name, *input)
554
- begin
555
- yield(*input)
556
- rescue Typingpool::Error::Argument => exception
557
- goodbye = "Could not make sense of #{name.to_s} "
558
- goodbye += input.map{|input| "'#{input}'" }.join(', ')
559
- goodbye += ". #{exception.message}"
560
- goodbye += '.' unless goodbye.match(/\.$/)
561
- abort goodbye
562
- end #begin
563
- end
564
- end #FriendlyExceptions
565
- module CLI
566
- class << self
567
- include App::FriendlyExceptions
568
- #Optionally takes an ostensible path to a config file, as passed
569
- #as a command-line option. Checks to make sure the file exists;
570
- #returns nil if does not, returns a Config instance if it
571
- #does. If no path is passed, the default config file is returned
572
- #(as retrieved by Config.file with no args).
573
- def config_from_arg(arg=nil)
574
- if arg
575
- path = File.expand_path(arg)
576
- return unless File.exists?(path) && File.file?(path)
577
- Config.file(path)
578
- else
579
- Config.file
580
- end #if option
581
- end
582
-
583
- #Outputs a friendly explanation of the --help option for
584
- #appending to script usage banners.
585
- def help_arg_explanation
586
- "`#{File.basename($PROGRAM_NAME)} --help` for more information."
587
- end
588
-
589
- #Converts a user arg into a Project instance, setting up or
590
- #consulting a Config along the way.
591
- # ==== Params
592
- # [arg] A user-supplied argument specifying either an absolute
593
- # path to a Project folder (Project#local) or the
594
- # name of a project folder within
595
- # [config]#transcripts.
596
- # [config] A Config instance. If [arg] is an absolute path,
597
- # will be modified -- Config#itranscripts will be
598
- # changed to match the implied transcripts dir.
599
- # ==== Errors
600
- # Will abort with a friendly message on any errors.
601
- # ==== Returns
602
- # A Project instance.
603
- def project_from_arg_and_config(arg, config)
604
- path = if (File.exists?(arg) && File.directory?(arg))
605
- config.transcripts = File.dirname(arg)
606
- arg
607
- else
608
- abort "No 'transcripts' dir specified in your config file and '#{arg}' is not a valid path" unless config.transcripts
609
- path = File.join(config.transcripts, arg)
610
- abort "No such project '#{arg}' in dir '#{config.transcripts}'" unless File.exists? path
611
- abort "'#{arg}' is not a directory at '#{path}'" unless File.directory? path
612
- path
613
- end
614
- project = with_friendly_exceptions('project name', File.basename(path)) do
615
- Typingpool::Project.new(File.basename(path), config)
616
- end
617
- abort "Not a project directory at '#{path}'" unless project.local
618
- project
619
- end
620
-
621
- end #class << self
622
- module Formatter
623
- require 'highline/import'
624
- def cli_bold(text)
625
- HighLine.color(text, :bold)
626
- end
627
-
628
- def cli_reverse(text)
629
- HighLine.color(text, :reverse)
630
- end
631
- end #Formatter
632
- end #CLI
536
+ require 'typingpool/app/friendlyexceptions'
537
+ require 'typingpool/app/cli'
633
538
  end #App
634
539
  end #Typingpool
@@ -0,0 +1,114 @@
1
+ module Typingpool
2
+ class Config
3
+
4
+ #The root level of the config file and all full config
5
+ #objects. Kept distinct from Config because other subclasses need
6
+ #to inherit from Config, and we don't want them inheriting the
7
+ #root level fields.
8
+ class Root < Config
9
+ local_path_reader :transcripts, :cache, :templates
10
+
11
+ class SFTP < Config
12
+ never_ends_in_slash_reader :path, :url
13
+ end
14
+
15
+ class Amazon < Config
16
+ never_ends_in_slash_reader :url
17
+ end
18
+
19
+ class Assign < Config
20
+ local_path_reader :templates
21
+ time_accessor :deadline, :approval, :lifetime
22
+
23
+ define_accessor(:reward) do |value|
24
+ value.to_s.match(/(\d+(\.\d+)?)|(\d*\.\d+)/) or raise Error::Argument::Format, "Format should be N.NN"
25
+ value
26
+ end
27
+
28
+ define_reader(:confirm) do |value|
29
+ next false if value.to_s.match(/(^n)|(^0)|(^false)/i)
30
+ next true if value.to_s.match(/(^y)|(^1)|(^true)/i)
31
+ next if value.to_s.empty?
32
+ raise Error::Argument::Format, "Format should be 'yes' or 'no'"
33
+ end
34
+
35
+ def qualify
36
+ self.qualify = (@param['qualify'] || []) unless @qualify
37
+ @qualify
38
+ end
39
+
40
+ def qualify=(specs)
41
+ @qualify = specs.map{|spec| Qualification.new(spec) }
42
+ end
43
+
44
+ def add_qualification(spec)
45
+ self.qualify.push(Qualification.new(spec))
46
+ end
47
+
48
+ def keywords
49
+ @param['keywords'] ||= []
50
+ end
51
+
52
+ def keywords=(array)
53
+ @param['keywords'] = array
54
+ end
55
+
56
+ class Qualification < Config
57
+ def initialize(spec)
58
+ @raw = spec
59
+ to_arg #make sure value parses
60
+ end
61
+
62
+ def to_s
63
+ @raw
64
+ end
65
+
66
+ def to_arg
67
+ [type, opts]
68
+ end
69
+
70
+ protected
71
+
72
+ def type
73
+ type = @raw.split(/\s+/)[0]
74
+ if RTurk::Qualification::TYPES[type.to_sym]
75
+ return type.to_sym
76
+ elsif (type.match(/\d/) || type.size >= 25)
77
+ return type
78
+ else
79
+ #Seems likely to be qualification typo: Not a known
80
+ #system qualification, all letters and less than 25
81
+ #chars
82
+ raise Error::Argument, "Unknown qualification type and does not appear to be a raw qualification type ID: '#{type.to_s}'"
83
+ end
84
+ end
85
+
86
+ def opts
87
+ args = @raw.split(/\s+/)
88
+ if (args.count > 3) || (args.count < 2)
89
+ raise Error::Argument, "Unexpected number of qualification tokens: #{@raw}"
90
+ end
91
+ args.shift
92
+ comparator(args[0]) or raise Error::Argument, "Unknown comparator '#{args[0]}'"
93
+ value = 1
94
+ value = args[1] if args.count == 2
95
+ return {comparator(args[0]) => value}
96
+ end
97
+
98
+ def comparator(value)
99
+ Hash[
100
+ '>' => :gt,
101
+ '>=' => :gte,
102
+ '<' => :lt,
103
+ '<=' => :lte,
104
+ '==' => :eql,
105
+ '!=' => :not,
106
+ 'true' => :eql,
107
+ 'exists' => :exists
108
+ ][value]
109
+ end
110
+ end #Qualification
111
+ end #Assign
112
+ end #Root
113
+ end #Config
114
+ end #Typingpool
@@ -99,9 +99,9 @@ module Typingpool
99
99
  # conf.sftp.url = 'http://luvrecording.s3.amazonaws.com/'
100
100
  # puts conf.sftp.url #'http://luvrecording.s3.amazonaws.com'
101
101
  #
102
- class Config
103
-
102
+ class Config
104
103
  require 'yaml'
104
+
105
105
  @@default_file = "~/.typingpool"
106
106
 
107
107
  def initialize(params)
@@ -158,29 +158,32 @@ module Typingpool
158
158
  end
159
159
  end
160
160
 
161
- def define_reader(*syms)
161
+ def define_reader(*syms, &block)
162
+ #explicit block passing to avoid sefault in 1.9.3-p362
162
163
  syms.each do |sym|
163
164
  define_method(sym) do
164
165
  value = @param[sym.to_s]
165
- yield(value)
166
+ block.call(value)
166
167
  end
167
168
  end
168
169
  end
169
170
 
170
- def define_writer(*syms)
171
+ def define_writer(*syms, &block)
172
+ #explicit block passing to avoid sefault in 1.9.3-p362
171
173
  syms.each do |sym|
172
174
  define_method("#{sym.to_s}=".to_sym) do |value|
173
- @param[sym.to_s] = yield(value)
175
+ @param[sym.to_s] = block.call(value)
174
176
  end
175
177
  end
176
178
  end
177
179
 
178
- def define_accessor(*syms)
180
+ def define_accessor(*syms, &block)
181
+ #explicit block passing to avoid sefault in 1.9.3-p362
179
182
  define_reader(*syms) do |value|
180
- yield(value) if value
183
+ block.call(value) if value
181
184
  end
182
185
  define_writer(*syms) do |value|
183
- yield(value)
186
+ block.call(value)
184
187
  end
185
188
  end
186
189
 
@@ -230,115 +233,6 @@ module Typingpool
230
233
  match = meth.to_s.match(/([^=]+)=$/) or return
231
234
  return match[1]
232
235
  end
233
-
234
- #The root level of the config file and all full config
235
- #objects. Kept distinct from Config because other subclasses need
236
- #to inherit from Config, and we don't want them inheriting the
237
- #root level fields.
238
- class Root < Config
239
- local_path_reader :transcripts, :cache, :templates
240
-
241
- class SFTP < Config
242
- never_ends_in_slash_reader :path, :url
243
- end
244
-
245
- class Amazon < Config
246
- never_ends_in_slash_reader :url
247
- end
248
-
249
- class Assign < Config
250
- local_path_reader :templates
251
- time_accessor :deadline, :approval, :lifetime
252
-
253
- define_accessor(:reward) do |value|
254
- value.to_s.match(/(\d+(\.\d+)?)|(\d*\.\d+)/) or raise Error::Argument::Format, "Format should be N.NN"
255
- value
256
- end
257
-
258
- define_reader(:confirm) do |value|
259
- next false if value.to_s.match(/(^n)|(^0)|(^false)/i)
260
- next true if value.to_s.match(/(^y)|(^1)|(^true)/i)
261
- next if value.to_s.empty?
262
- raise Error::Argument::Format, "Format should be 'yes' or 'no'"
263
- end
264
-
265
- def qualify
266
- self.qualify = (@param['qualify'] || []) unless @qualify
267
- @qualify
268
- end
269
-
270
- def qualify=(specs)
271
- @qualify = specs.map{|spec| Qualification.new(spec) }
272
- end
273
-
274
- def add_qualification(spec)
275
- self.qualify.push(Qualification.new(spec))
276
- end
277
-
278
- def keywords
279
- @param['keywords'] ||= []
280
- end
281
-
282
- def keywords=(array)
283
- @param['keywords'] = array
284
- end
285
-
286
- class Qualification < Config
287
- def initialize(spec)
288
- @raw = spec
289
- to_arg #make sure value parses
290
- end
291
-
292
- def to_s
293
- @raw
294
- end
295
-
296
- def to_arg
297
- [type, opts]
298
- end
299
-
300
- protected
301
-
302
- def type
303
- type = @raw.split(/\s+/)[0]
304
- if RTurk::Qualification::TYPES[type.to_sym]
305
- return type.to_sym
306
- elsif (type.match(/\d/) || type.size >= 25)
307
- return type
308
- else
309
- #Seems likely to be qualification typo: Not a known
310
- #system qualification, all letters and less than 25
311
- #chars
312
- raise Error::Argument, "Unknown qualification type and does not appear to be a raw qualification type ID: '#{type.to_s}'"
313
- end
314
- end
315
-
316
- def opts
317
- args = @raw.split(/\s+/)
318
- if (args.count > 3) || (args.count < 2)
319
- raise Error::Argument, "Unexpected number of qualification tokens: #{@raw}"
320
- end
321
- args.shift
322
- comparator(args[0]) or raise Error::Argument, "Unknown comparator '#{args[0]}'"
323
- value = 1
324
- value = args[1] if args.count == 2
325
- return {comparator(args[0]) => value}
326
- end
327
-
328
- def comparator(value)
329
- Hash[
330
- '>' => :gt,
331
- '>=' => :gte,
332
- '<' => :lt,
333
- '<=' => :lte,
334
- '==' => :eql,
335
- '!=' => :not,
336
- 'true' => :eql,
337
- 'exists' => :exists
338
- ][value]
339
- end
340
- end #Qualification
341
- end #Assign
342
- end #Root
343
236
  end #Config
237
+ require 'typingpool/config/root'
344
238
  end #Typingpool
@@ -0,0 +1,84 @@
1
+ module Typingpool
2
+ class Filer
3
+
4
+ #Convenience wrapper for audio files.You can convert to mp3s,
5
+ #split into multiple files, and dynamically read the bitrate.
6
+ class Audio < Filer
7
+ require 'open3'
8
+
9
+ #Does the file have a '.mp3' extension?
10
+ def mp3?
11
+ File.extname(@path).downcase.eql?('.mp3')
12
+ end
13
+
14
+ #Convert to mp3 via ffmpeg.
15
+ # ==== Params
16
+ # [dest] Filer object corresponding to the path the mp3 version
17
+ # should end up at.
18
+ # [bitrate] If passed, bitrate should be an integer
19
+ # corresponding to kb/s. If not, we use the bitrate
20
+ # from the current file or, if that can't be read,
21
+ # default to 192kbps. Does not check if the file is
22
+ # already an mp3. Returns a new Filer::Audio
23
+ # representing the new mp3 file.
24
+ # ==== Returns
25
+ # Filer::Audio containing the new mp3.
26
+ def to_mp3(dest=self.dir.file("#{File.basename(@path, '.*') }.mp3"), bitrate=nil)
27
+ bitrate ||= self.bitrate || 192
28
+ Utility.system_quietly('ffmpeg', '-i', @path, '-acodec', 'libmp3lame', '-ab', "#{bitrate}k", '-ac', '2', dest)
29
+ File.exists?(dest) or raise Error::Shell, "Could not found output from `ffmpeg` on #{path}"
30
+ self.class.new(dest.path)
31
+ end
32
+
33
+ #Reads the bitrate of the audio file via ffmpeg. Returns an
34
+ #integer corresponding to kb/s, or nil if the bitrate could not
35
+ #be determined.
36
+ def bitrate
37
+ out, err, status = Open3.capture3('ffmpeg', '-i', @path)
38
+ bitrate = err.match(/(\d+) kb\/s/)
39
+ return bitrate ? bitrate[1].to_i : nil
40
+ end
41
+
42
+ #Splits an mp3 into smaller files.
43
+ # ==== Params
44
+ # [interval_in_min_dot_seconds] Split the file into chunks this
45
+ # large. The interval should be of the format
46
+ # minute.seconds, for example 2 minutes 15 seconds
47
+ # would be written as "2.15". For further details on
48
+ # interval format, consult the documentation for
49
+ # mp3split, a command-line unix utility.
50
+ # [basename] Name the new chunks using this base. Default is the
51
+ # basename of the original file.
52
+ # [dest] Destination directory for the new chunks as a
53
+ # Filer::Dir. Default is the same directory as the
54
+ # original file.
55
+ # ==== Returns
56
+ # Filer::Files containing the new files.
57
+ def split(interval_in_min_dot_seconds, basename=File.basename(path, '.*'), dest=dir)
58
+ #We have to cd into the wrapfile directory and do everything
59
+ #there because old/packaged versions of mp3splt were
60
+ #retarded at handling absolute directory paths
61
+ ::Dir.chdir(dir.path) do
62
+ Utility.system_quietly('mp3splt', '-t', interval_in_min_dot_seconds, '-o', "#{basename}.@m.@s", File.basename(path))
63
+ end
64
+ files = Filer::Files::Audio.new(dir.select{|file| File.basename(file.path).match(/^#{Regexp.escape(basename) }\.\d+\.\d+\.mp3$/) })
65
+ if files.to_a.empty?
66
+ raise Error::Shell, "Could not find output from `mp3splt` on #{path}"
67
+ end
68
+ if dest.path != dir.path
69
+ files.mv!(dest)
70
+ end
71
+ files.sort
72
+ end
73
+
74
+ #Extracts from the filename the offset time of the chunk
75
+ #relative to the original from which it was split. Format is
76
+ #minute.seconds. Suitable for use on files created by 'split'
77
+ #method.
78
+ def offset
79
+ match = File.basename(@path).match(/\d+\.\d\d\b/)
80
+ return match[0] if match
81
+ end
82
+ end #Audio
83
+ end #Filer
84
+ end #Typingpool
@@ -0,0 +1,57 @@
1
+ module Typingpool
2
+ class Filer
3
+
4
+ #Convenience wrapper for CSV files. Makes them Enumerable, so you
5
+ #can iterate through rows with each, map, select, etc. You can
6
+ #also modify in place with each!. See Filer base class for other
7
+ #methods.
8
+ class CSV < Filer
9
+ include Enumerable
10
+ require 'csv'
11
+
12
+ #Reads into an array of hashes, with hash keys determined by the
13
+ #first row of the CSV file. Parsing rules are the default for
14
+ #CSV.parse.
15
+ def read
16
+ raw = super or return []
17
+ rows = ::CSV.parse(raw.to_s)
18
+ headers = rows.shift or raise Error::File, "No CSV at #{@path}"
19
+ rows.map{|row| Utility.array_to_hash(row, headers) }
20
+ end
21
+
22
+ #Takes array of hashes followed by optional list of keys (by
23
+ #default keys are determined by looking at all the
24
+ #hashes). Lines are written per the defaults of
25
+ #CSV.generate_line.
26
+ def write(hashes, headers=hashes.map{|h| h.keys}.flatten.uniq)
27
+ super(
28
+ ::CSV.generate_line(headers, :encoding => @encoding) +
29
+ hashes.map do |hash|
30
+ ::CSV.generate_line(headers.map{|header| hash[header] }, :encoding => @encoding)
31
+ end.join
32
+ )
33
+ end
34
+
35
+ #Takes an array of arrays, corresponding to the rows, and a list
36
+ #of headers/keys to write at the top.
37
+ def write_arrays(arrays, headers)
38
+ write(arrays.map{|array| Utility.array_to_hash(array, headers) }, headers)
39
+ end
40
+
41
+ #Enumerate through the rows, with each row represented by a
42
+ #hash.
43
+ def each
44
+ read.each do |row|
45
+ yield row
46
+ end
47
+ end
48
+
49
+ #Same as each, but any changes to the rows will be written back
50
+ #out to the underlying CSV file.
51
+ def each!
52
+ #each_with_index doesn't return the array, so we have to use each
53
+ write(each{|hash| yield(hash) })
54
+ end
55
+ end #CSV
56
+ end #Filer
57
+ end #Typingpool