fight_csv 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
data/.document ADDED
@@ -0,0 +1,5 @@
1
+ lib/**/*.rb
2
+ bin/*
3
+ -
4
+ features/**/*.feature
5
+ LICENSE.txt
data/.rvmrc ADDED
@@ -0,0 +1 @@
1
+ rvm use ruby-1.9.2-p180@fight_csv
data/Gemfile ADDED
@@ -0,0 +1,16 @@
1
+ source :rubygems
2
+ # Add dependencies required to use your gem here.
3
+ # Example:
4
+ # gem "activesupport", ">= 2.3.5"
5
+
6
+ # Add dependencies to develop your gem here.
7
+ # Include everything needed to run rake, tests, features, etc.
8
+ group :development do
9
+ gem "jeweler", "~> 1.6.0"
10
+ gem 'awesome_print'
11
+ gem 'simplecov'
12
+ gem 'rake'
13
+ end
14
+
15
+ gem 'constructable'
16
+ gem 'activesupport'
data/Gemfile.lock ADDED
@@ -0,0 +1,26 @@
1
+ GEM
2
+ remote: http://rubygems.org/
3
+ specs:
4
+ activesupport (3.0.9)
5
+ awesome_print (0.4.0)
6
+ constructable (0.3.4)
7
+ git (1.2.5)
8
+ jeweler (1.6.2)
9
+ bundler (~> 1.0)
10
+ git (>= 1.2.5)
11
+ rake
12
+ rake (0.9.2)
13
+ simplecov (0.4.2)
14
+ simplecov-html (~> 0.4.4)
15
+ simplecov-html (0.4.5)
16
+
17
+ PLATFORMS
18
+ ruby
19
+
20
+ DEPENDENCIES
21
+ activesupport
22
+ awesome_print
23
+ constructable
24
+ jeweler (~> 1.6.0)
25
+ rake
26
+ simplecov
data/LICENSE.txt ADDED
@@ -0,0 +1,20 @@
1
+ Copyright (c) 2011 Manuel Korfmann
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.
data/README.md ADDED
@@ -0,0 +1,160 @@
1
+ # Fight CSV!
2
+
3
+ It's 2011, and parsing CSV with Ruby still sucks? Enter FightCSV! It
4
+ will take the cumbersome out of your CSV parsing, while keeping the
5
+ awesome! Want some taste of that juicy fresh? Check out this example:
6
+
7
+ Consider you have a csv file called log_entries.csv which looks like
8
+ this:
9
+
10
+ ```
11
+ Date,Person,Client/Project,Minutes,Tags,Billable
12
+ 2011-08-15,John Doe,handsomelabs,60,blogpost,no
13
+ 2011-08-15,Max Powers,beerbrewing,60,meeting,yes
14
+ 2011-08-15,Tyler Durden,babysitting,180,"concepting, research",yes
15
+ 2011-08-15,Hulk Hero,gardening,60,"meeting, research",no
16
+ 2011-08-15,John Doe,handsomelabs,60,coding,yes
17
+ 2011-08-08,John Doe,handsomelabs,60,"blabla, meeting",yes
18
+ ```
19
+
20
+ ## Schema
21
+
22
+ Now you can define a class representing a row of the file. You only need
23
+ to include ```FightCSV::Record```.
24
+
25
+ ```ruby
26
+ class LogEntry
27
+ include FightCSV::Record
28
+ end
29
+ ```
30
+
31
+ But of course you want the values from each row to behave like proper
32
+ Ruby objects. This can be easily achieved by defining a schema in the
33
+ ```LogEntry``` class:
34
+
35
+ ```ruby
36
+ class LogEntry
37
+ include FightCSV::Record
38
+ schema do
39
+ field "Name"
40
+ field "Client/Project", {
41
+ identifier: :project
42
+ }
43
+ end
44
+ end
45
+ ```
46
+
47
+ Now the LogEntry objects will have a ```name``` method corresponding to
48
+ the column called "Name" and a ```project``` method corresponding to the
49
+ column called "Client/Project".
50
+
51
+ But sometimes you don't only want to adjust the field names, but also
52
+ the values. In this case FightCSV offers converters. The "Billable"
53
+ column seems to represent boolean values, so let's tackle that:
54
+
55
+ ```ruby
56
+ class LogEntry
57
+ include FightCSV::Record
58
+ schema do
59
+ field "Name"
60
+ field "Client/Project", {
61
+ identifier: :project
62
+ }
63
+
64
+ field "Billable", {
65
+ converter: ->(string) { string == "yes" ? true : false }
66
+ }
67
+ end
68
+ end
69
+
70
+ ```
71
+
72
+ Often when converting something, we assume that it has a certain format.
73
+ The "Date" column for example should always be of the format
74
+ ```/\d{2}\.\d{2}\.\d{4}/```. A validation can easily be added to a column
75
+ with FightCSV:
76
+
77
+ ```ruby
78
+ class LogEntry
79
+ include FightCSV::Record
80
+ schema do
81
+ field "Name"
82
+ field "Client/Project", {
83
+ identifier: :project
84
+ }
85
+
86
+ field "Billable", {
87
+ converter: ->(string) { string == "yes" ? true : false }
88
+ }
89
+
90
+ field "Date", {
91
+ validate: /\d{2}\.\d{2}\.\d{4}/,
92
+ converter: ->(string) { Date.parse(string) }
93
+ }
94
+ end
95
+ end
96
+ ```
97
+
98
+ The complete schema:
99
+
100
+ ```ruby
101
+ class LogEntry
102
+ include FightCSV::Record
103
+ schema do
104
+ field "Name"
105
+ field "Client/Project", {
106
+ identifier: :project
107
+ }
108
+
109
+ field "Billable", {
110
+ converter: ->(string) { string == "yes" ? true : false }
111
+ }
112
+
113
+ field "Date", {
114
+ validate: /\d{2}\.\d{2}\.\d{4}/,
115
+ converter: ->(string) { Date.parse(string) }
116
+ }
117
+
118
+ field "Tags", {
119
+ converter: ->(string) { string.split(",") }
120
+ }
121
+
122
+ field "Minutes", {
123
+ validate: /\d+/,
124
+ converter: ->(string) { string.to_i }
125
+ }
126
+ end
127
+ end
128
+ ```
129
+
130
+ ## Parsing CSV
131
+
132
+ With the schema definition you're finally able to parse some CSV. There
133
+ are two possible ways of doing this:
134
+
135
+ 1. ```LogEntry.records``` will return an array with all rows
136
+ mapped to instances of ```LogEntry```.
137
+
138
+ 2. ```LogEntry.import``` will return an enumerator which will pass the same ```LogEntry``` instance with the
139
+ row changed for every iteration.
140
+
141
+ ```ruby
142
+ LogEntry.import(csv).map(&:minutes).reduce(:+)
143
+ #=> 780
144
+ ```
145
+ Doing so you can avoid memory leaks on big csv documents.
146
+
147
+
148
+ ## Contributing to fight\_csv
149
+ * Check out the latest master to make sure the feature hasn't been implemented or the bug hasn't been fixed yet
150
+ * Check out the issue tracker to make sure someone already hasn't requested it and/or contributed it
151
+ * Fork the project
152
+ * Commit and push until you are happy with your contribution
153
+ * Make sure to add tests for it. This is important so I don't break it in a future version unintentionally.
154
+ * Please try not to mess with the Rakefile, version, or history. If you want to have your own version, or is otherwise necessary, that is fine, but please isolate to its own commit so I can cherry-pick around it.
155
+
156
+ ## Copyright
157
+
158
+ Copyright (c) 2011 Manuel Korfmann. See LICENSE.txt for
159
+ further details.
160
+
data/Rakefile ADDED
@@ -0,0 +1,40 @@
1
+ # encoding: utf-8
2
+
3
+ require 'rubygems'
4
+ require 'bundler'
5
+ begin
6
+ Bundler.setup(:default, :development)
7
+ rescue Bundler::BundlerError => e
8
+ $stderr.puts e.message
9
+ $stderr.puts "Run `bundle install` to install missing gems"
10
+ exit e.status_code
11
+ end
12
+ require 'rake'
13
+
14
+ require 'jeweler'
15
+ Jeweler::Tasks.new do |gem|
16
+ # gem is a Gem::Specification... see http://docs.rubygems.org/read/chapter/20 for more options
17
+ gem.name = "fight_csv"
18
+ gem.homepage = "http://github.com/mkorfmann/fight_csv"
19
+ gem.license = "MIT"
20
+ gem.summary = %Q{JSON-Schema + ActiveModel for CSV}
21
+ gem.description = %Q{
22
+ Provides a nice DSL to describe your CSV document.
23
+ CSV documents can be validated against this description.
24
+ You can easily define types like Integer or Array for CSV through converters.
25
+ }
26
+ gem.email = "manu@korfmann.info"
27
+ gem.authors = ["Manuel Korfmann"]
28
+ # dependencies defined in Gemfile
29
+ end
30
+ Jeweler::RubygemsDotOrgTasks.new
31
+
32
+ require 'rake/testtask'
33
+ Rake::TestTask.new(:test) do |test|
34
+ test.libs << 'lib' << 'test'
35
+ test.pattern = 'test/**/test_*.rb'
36
+ test.verbose = true
37
+ end
38
+
39
+
40
+ task :default => :test
data/VERSION ADDED
@@ -0,0 +1 @@
1
+ 0.1.0
@@ -0,0 +1,46 @@
1
+ require '../lib/fight_csv'
2
+
3
+ csv = <<-CSV
4
+ Date,Person,Client/Project,Minutes,Tags,Billable
5
+ 2011-08-15,John Doe,handsomelabs,60,blogpost,no
6
+ 2011-08-15,Max Powers,beerbrewing,60,meeting,yes
7
+ 2011-08-15,Tyler Durden,babysitting,180,"concepting, research",yes
8
+ 2011-08-15,Hulk Hero,gardening,60,"meeting, research",no
9
+ 2011-08-15,John Doe,handsomelabs,60,coding,yes
10
+ 2011-08-08,John Doe,handsomelabs,60,"blabla, meeting",yes
11
+ CSV
12
+
13
+ class LogEntry
14
+ include FightCSV::Record
15
+ schema do
16
+ field "Person"
17
+ field "Client/Project", {
18
+ identifier: :project
19
+ }
20
+
21
+ field "Billable", {
22
+ converter: ->(string) { string == "yes" ? true : false }
23
+ }
24
+
25
+ field "Date", {
26
+ validate: /\d{2}\.\d{2}\.\d{4}/,
27
+ converter: ->(string) { Date.parse(string) }
28
+ }
29
+
30
+ field "Tags", {
31
+ converter: ->(string) { string.split(",") }
32
+ }
33
+
34
+ field "Minutes", {
35
+ validate: /\d+/,
36
+ converter: ->(string) { string.to_i }
37
+ }
38
+ end
39
+ end
40
+
41
+
42
+ records = LogEntry.records csv
43
+
44
+ # Persons who have worked on billable projects
45
+ billable_entries = records.select(&:billable)
46
+ puts billable_entries.map(&:person).uniq.join(" - ")
data/fight_csv.gemspec ADDED
@@ -0,0 +1,84 @@
1
+ # Generated by jeweler
2
+ # DO NOT EDIT THIS FILE DIRECTLY
3
+ # Instead, edit Jeweler::Tasks in Rakefile, and run 'rake gemspec'
4
+ # -*- encoding: utf-8 -*-
5
+
6
+ Gem::Specification.new do |s|
7
+ s.name = %q{fight_csv}
8
+ s.version = "0.1.0"
9
+
10
+ s.required_rubygems_version = Gem::Requirement.new(">= 0") if s.respond_to? :required_rubygems_version=
11
+ s.authors = ["Manuel Korfmann"]
12
+ s.date = %q{2011-08-18}
13
+ s.description = %q{
14
+ Provides a nice DSL to describe your CSV document.
15
+ CSV documents can be validated against this description.
16
+ You can easily define types like Integer or Array for CSV through converters.
17
+ }
18
+ s.email = %q{manu@korfmann.info}
19
+ s.extra_rdoc_files = [
20
+ "LICENSE.txt",
21
+ "README.md"
22
+ ]
23
+ s.files = [
24
+ ".document",
25
+ ".rvmrc",
26
+ "Gemfile",
27
+ "Gemfile.lock",
28
+ "LICENSE.txt",
29
+ "README.md",
30
+ "Rakefile",
31
+ "VERSION",
32
+ "examples/timetracking.rb",
33
+ "fight_csv.gemspec",
34
+ "lib/fight_csv.rb",
35
+ "lib/fight_csv/data_source.rb",
36
+ "lib/fight_csv/field.rb",
37
+ "lib/fight_csv/record.rb",
38
+ "lib/fight_csv/schema.rb",
39
+ "tags",
40
+ "test/fixtures/cocktails.csv",
41
+ "test/fixtures/prog_lang_schema.rb",
42
+ "test/fixtures/programming_languages.csv",
43
+ "test/fixtures/recipes.csv",
44
+ "test/helper.rb",
45
+ "test/test_field.rb",
46
+ "test/test_fight_csv.rb",
47
+ "test/test_record.rb",
48
+ "test/test_schema.rb"
49
+ ]
50
+ s.homepage = %q{http://github.com/mkorfmann/fight_csv}
51
+ s.licenses = ["MIT"]
52
+ s.require_paths = ["lib"]
53
+ s.rubygems_version = %q{1.3.7}
54
+ s.summary = %q{JSON-Schema + ActiveModel for CSV}
55
+
56
+ if s.respond_to? :specification_version then
57
+ current_version = Gem::Specification::CURRENT_SPECIFICATION_VERSION
58
+ s.specification_version = 3
59
+
60
+ if Gem::Version.new(Gem::VERSION) >= Gem::Version.new('1.2.0') then
61
+ s.add_runtime_dependency(%q<constructable>, [">= 0"])
62
+ s.add_runtime_dependency(%q<activesupport>, [">= 0"])
63
+ s.add_development_dependency(%q<jeweler>, ["~> 1.6.0"])
64
+ s.add_development_dependency(%q<awesome_print>, [">= 0"])
65
+ s.add_development_dependency(%q<simplecov>, [">= 0"])
66
+ s.add_development_dependency(%q<rake>, [">= 0"])
67
+ else
68
+ s.add_dependency(%q<constructable>, [">= 0"])
69
+ s.add_dependency(%q<activesupport>, [">= 0"])
70
+ s.add_dependency(%q<jeweler>, ["~> 1.6.0"])
71
+ s.add_dependency(%q<awesome_print>, [">= 0"])
72
+ s.add_dependency(%q<simplecov>, [">= 0"])
73
+ s.add_dependency(%q<rake>, [">= 0"])
74
+ end
75
+ else
76
+ s.add_dependency(%q<constructable>, [">= 0"])
77
+ s.add_dependency(%q<activesupport>, [">= 0"])
78
+ s.add_dependency(%q<jeweler>, ["~> 1.6.0"])
79
+ s.add_dependency(%q<awesome_print>, [">= 0"])
80
+ s.add_dependency(%q<simplecov>, [">= 0"])
81
+ s.add_dependency(%q<rake>, [">= 0"])
82
+ end
83
+ end
84
+
@@ -0,0 +1,23 @@
1
+ require 'csv'
2
+ module FightCSV
3
+ class DataSource
4
+ include Enumerable
5
+
6
+ ALLOWED_OPTIONS = [:col_sep, :row_sep, :quote_char]
7
+
8
+ constructable :header, default: ->{true}, readable: true
9
+ constructable :io, readable: true
10
+ constructable :csv_options,
11
+ accessible: true,
12
+ default: ->{ Hash.new }
13
+
14
+ def each
15
+ csv = CSV.new(self.io, Hash[csv_options.select { |opt| ALLOWED_OPTIONS.include opt }])
16
+ additions = {}
17
+ additions[:header] = csv.shift if self.header
18
+ csv.each do |row|
19
+ yield row, additions
20
+ end
21
+ end
22
+ end
23
+ end
@@ -0,0 +1,64 @@
1
+ module FightCSV
2
+ class Field
3
+ constructable :converter, validate_type: Proc, accessible: true
4
+ constructable :identifier, validate_type: Symbol, accessible: true
5
+ constructable :validator, accessible: true, default: ->{/.*/}
6
+
7
+ attr_accessor :matcher
8
+
9
+ def initialize(matcher, options = {})
10
+ @matcher = matcher
11
+ end
12
+
13
+ def identifier
14
+ if super
15
+ super
16
+ else
17
+ case self.matcher
18
+ when String
19
+ self.matcher.downcase.to_sym
20
+ else
21
+ raise ArgumentError, "Please specify an identifier"
22
+ end
23
+ end
24
+ end
25
+
26
+ def validate(row, header = nil)
27
+ match = self.match(row, header).to_s
28
+ if self.validator.respond_to?(:call)
29
+ result = self.validator.call(match)
30
+ verb = "pass"
31
+ else
32
+ result = (self.validator === match)
33
+ verb = "match"
34
+ end
35
+
36
+ unless result
37
+ { valid: false, error: "#{self.identifier.inspect} must #{verb} #{self.validator}, but was #{match.inspect}"}
38
+ else
39
+ { valid: true }
40
+ end
41
+ end
42
+
43
+ def match(row, header = nil)
44
+ case self.matcher
45
+ when Integer
46
+ row[matcher-1]
47
+ else
48
+ raise ArgumentError, 'No header is provided, but a matcher other than an Integer requires one' unless header
49
+ index = header.index { |n| self.matcher === n }
50
+ index ? row[index] : nil
51
+ end
52
+ end
53
+
54
+ def process(row, header = nil)
55
+ if match = self.match(row, header)
56
+ self.converter ? self.converter.call(match) : match
57
+ end
58
+ end
59
+
60
+ def ivar_symbol
61
+ :"@#{self.identifier}"
62
+ end
63
+ end
64
+ end
@@ -0,0 +1,91 @@
1
+ module FightCSV
2
+ module Record
3
+ extend ActiveSupport::Concern
4
+
5
+ module ClassMethods
6
+ def schema=(schema)
7
+ @schema = schema
8
+ end
9
+
10
+ def records(io)
11
+ data_source = DataSource.new(io: io)
12
+ data_source.map { |row,additions|self.new(row, additions) }
13
+ end
14
+
15
+ def import(io)
16
+ Enumerator.new do |yielder|
17
+ record = self.new
18
+ data_source = DataSource.new(io: io)
19
+ data_source.each do |row, additions|
20
+ record.header = additions[:header]
21
+ record.row = row
22
+ yielder << record
23
+ end
24
+ end
25
+ end
26
+
27
+ def schema(filename = nil, &block)
28
+ if filename || block
29
+ @schema = Schema.new(filename, &block)
30
+ else
31
+ @schema
32
+ end
33
+ end
34
+ end
35
+
36
+ module InstanceMethods
37
+ constructable :schema, validate_type: Schema, accessible: true
38
+ constructable :header, accessible: true
39
+
40
+ attr_accessor :row
41
+ attr_reader :errors
42
+
43
+
44
+ def initialize(row = nil,options = {})
45
+ @schema ||= self.class.schema
46
+ self.row = row if row
47
+ end
48
+
49
+ def row=(raw_row)
50
+ @raw_row = raw_row
51
+ @row = Hash[self.schema.fields.map { |field| [field.identifier,field.process(@raw_row, @header)] }]
52
+ end
53
+
54
+ def fields
55
+ Hash[schema.fields.map { |field| [field.identifier, self.send(field.identifier)] }]
56
+ end
57
+
58
+ def schema=(schema)
59
+ super
60
+ self.row = @raw_row
61
+ end
62
+
63
+ def valid?
64
+ validation = self.validate
65
+ @errors = validate[:errors]
66
+ validation[:valid]
67
+ end
68
+
69
+ def validate
70
+ self.schema.fields.inject({valid: true, errors: []}) do |validation_hash, field|
71
+ validation_of_field = field.validate(@raw_row, @header)
72
+ validation_hash[:valid] &&= validation_of_field[:valid]
73
+ validation_hash[:errors] << validation_of_field[:error] if validation_of_field[:error]
74
+ validation_hash
75
+ end
76
+ end
77
+
78
+ def method_missing(meth, *args, &block)
79
+ if field = schema.fields.find { |field| /#{field.identifier}(=)?/ === meth }
80
+ if $1 == '='
81
+ @row[field.identifier] = args.first
82
+ else
83
+ @row[field.identifier]
84
+ end
85
+ else
86
+ super
87
+ end
88
+ end
89
+ end
90
+ end
91
+ end
@@ -0,0 +1,17 @@
1
+ module FightCSV
2
+ class Schema
3
+ attr_accessor :fields
4
+ def initialize(filename = nil, &block)
5
+ self.fields = Array.new
6
+ if String === filename
7
+ self.instance_eval { eval(File.read(filename)) }
8
+ elsif block
9
+ self.instance_eval &block
10
+ end
11
+ end
12
+
13
+ def field(fieldname, constructor_hash = {})
14
+ self.fields << Field.new(fieldname, constructor_hash)
15
+ end
16
+ end
17
+ end
data/lib/fight_csv.rb ADDED
@@ -0,0 +1,10 @@
1
+ $LOAD_PATH << File.dirname(File.expand_path(__FILE__))
2
+ require 'constructable'
3
+ require 'active_support'
4
+ require 'fight_csv/schema'
5
+ require 'fight_csv/data_source'
6
+ require 'fight_csv/field'
7
+ require 'fight_csv/record'
8
+
9
+ module FightCSV
10
+ end