conformist 0.0.3 → 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -1,5 +1,15 @@
1
1
  # CHANGELOG
2
2
 
3
+ ## 0.1.0 / 2012-01-05
4
+
5
+ * Added anonymous schemas.
6
+ * Added `Conformist::Schema::Methods#conform` for lazily applying schema to input.
7
+ * Added capability to access columns with methods.
8
+ * FasterCSV is no longer included, use `require 'fastercsv'` instead.
9
+ * `include Conformist::Base` has been removed.
10
+ * `Conformist.foreach` has been removed.
11
+ * `Conformist::Base::ClassMethods#load` has been removed.
12
+
3
13
  ## 0.0.3 / 2011-05-07
4
14
 
5
15
  * Inheriting from a class which mixes in Conformist::Base gives you access to all of the superclasses' columns.
data/README.md ADDED
@@ -0,0 +1,299 @@
1
+ # Conformist
2
+
3
+ Bend CSVs to your will. Stop using array indexing and start using declarative schemas. Declarative schemas are easier to understand, quicker to setup and independent of I/O.
4
+
5
+ ![](http://f.cl.ly/items/00191n3O1J2E1a342F1L/conformist.jpg)
6
+
7
+ ## Quick and Dirty Examples
8
+
9
+ Open a CSV file and declare a schema.
10
+
11
+ ``` ruby
12
+ require 'csv'
13
+ require 'conformist'
14
+
15
+ csv = CSV.open '~/transmitters.csv'
16
+ schema = Conformist.new do
17
+ column :callsign, 1
18
+ column :latitude, 1, 2, 3
19
+ column :longitude, 3, 4, 5
20
+ column :name, 0 do |value|
21
+ value.upcase
22
+ end
23
+ end
24
+ ```
25
+
26
+ Insert the transmitters into a SQLite database.
27
+
28
+ ``` ruby
29
+ require 'sqlite3'
30
+
31
+ db = SQLite3::Database.new 'transmitters.db'
32
+ schema.conform(csv).each do |transmitter|
33
+ db.execute "INSERT INTO transmitters (callsign, ...) VALUES ('#{transmitter.callsign}', ...);"
34
+ end
35
+ ```
36
+
37
+ Only insert the transmitters with the name "Mount Cooth-tha" using ActiveRecord or DataMapper.
38
+
39
+ ``` ruby
40
+ transmitters = schema.conform(csv).select do |transmitter|
41
+ transmitter.name == 'Mount Coot-tha'
42
+ end
43
+ transmitter.each do |transmitter|
44
+ Transmitter.create! transmitter.attributes
45
+ end
46
+ ```
47
+
48
+ Source from multiple, different input files and insert transmitters together into a single database.
49
+
50
+ ``` ruby
51
+ require 'conformist'
52
+ require 'csv'
53
+ require 'sqlite3'
54
+
55
+ au_schema = Conformist.new do
56
+ column :callsign, 8
57
+ column :latitude, 10
58
+ end
59
+ us_schema = Conformist.new do
60
+ column :callsign, 1
61
+ column :latitude, 1, 2, 3
62
+ end
63
+
64
+ au_csv = CSV.open '~/au/transmitters.csv'
65
+ us_csv = CSV.open '~/us/transmitters.csv'
66
+
67
+ db = SQLite3::Database.new 'transmitters.db'
68
+
69
+ [au_schema.conform(au_csv), us_schema.conform(us_csv)].each do |schema|
70
+ schema.each do |transmitter|
71
+ db.execute "INSERT INTO transmitters (callsign, ...) VALUES ('#{transmitter.callsign}', ...);"
72
+ end
73
+ end
74
+ ```
75
+
76
+ For **more examples** see `test/fixtures`, `test/schemas` and `test/unit/integration_test.rb`.
77
+
78
+ ## Installation
79
+
80
+ Conformist is available as a gem. Install it at the command line.
81
+
82
+ ``` sh
83
+ $ [sudo] gem install conformist
84
+ ```
85
+
86
+ Or add it to your Gemfile and run `$ bundle install`.
87
+
88
+ ``` ruby
89
+ gem 'conformist'
90
+ ```
91
+
92
+ ## Usage
93
+
94
+ ### Anonymous Schema
95
+
96
+ Anonymous schemas are quick to declare and don't have the overhead of creating an explicit class.
97
+
98
+ ``` ruby
99
+ citizen = Conformist.new do
100
+ column :name, 0, 1
101
+ column :email, 2
102
+ end
103
+
104
+ citizen.conform [['Tate', 'Johnson', 'tate@tatey.com']]
105
+ ```
106
+
107
+ ### Class Schema
108
+
109
+ Class schemas are explicit. Class schemas were the only type available in earlier versions of Conformist.
110
+
111
+ ``` ruby
112
+ class Citizen
113
+ extend Conformist
114
+
115
+ column :name, 0, 1
116
+ column :email, 2
117
+ end
118
+
119
+ Citizen.conform [['Tate', 'Johnson', 'tate@tatey.com']]
120
+ ```
121
+
122
+ ### Conform
123
+
124
+ Conform is the principle method for lazily applying a schema to the given input.
125
+
126
+ ``` ruby
127
+ enumerator = schema.conform CSV.open('~/file.csv')
128
+ enumerator.each do |row|
129
+ puts row.attributes
130
+ end
131
+ ```
132
+
133
+ #### Input
134
+
135
+ `#conform` expects any object that responds to `#each` to return an array-like object.
136
+
137
+ ``` ruby
138
+ CSV.open('~/file.csv').responds_to? :each # => true
139
+ [[], [], []].responds_to? :each # => true
140
+ ```
141
+
142
+ #### Enumerator
143
+
144
+ `#conform` is lazy, returning an [Enumerator](http://www.ruby-doc.org/core-1.9.3/Enumerator.html). Input is not parsed until you call `#each`, `#map` or any method defined in [Enumerable](http://www.ruby-doc.org/core-1.9.3/Enumerable.html). That means schemas can be assigned now and evaluated later. `#each` has the lowest memory footprint because it does not build a collection.
145
+
146
+ #### Struct
147
+
148
+ The argument passed into the block is a struct-like object. You can access columns as methods or keys. Columns were only accessible as keys in earlier versions of Conformist. Methods are now the preferred syntax.
149
+
150
+ ``` ruby
151
+ citizen[:name] # => "Tate Johnson"
152
+ citizen.name # => "Tate Johnson"
153
+ ```
154
+
155
+ For convenience the `#attributes` method returns a hash of key-value pairs suitable for creating ActiveRecord or DataMapper records.
156
+
157
+ ``` ruby
158
+ citizen.attributes # => {:name => "Tate Johnson", :email => "tate@tatey.com"}
159
+ ```
160
+
161
+ ### One Column
162
+
163
+ Maps the first column in the input file to `:first_name`. Column indexing starts at zero.
164
+
165
+ ``` ruby
166
+ column :first_name, 0
167
+ ```
168
+
169
+ ### Many Columns
170
+
171
+ Maps the first and second columns in the input file to `:name`.
172
+
173
+ ``` ruby
174
+ column :name, 0, 1
175
+ ```
176
+
177
+ Indexing is completely arbitrary and you can map any combination.
178
+
179
+ ``` ruby
180
+ column :name_and_city 0, 1, 2
181
+ ```
182
+
183
+ Many columns are implicitly concatenated. Behaviour can be changed by passing a block. See *preprocessing*.
184
+
185
+ ### Preprocessing
186
+
187
+ Sometimes values need to be manipulated before they're conformed. Passing a block gets access to values. The return value of the block becomes the conformed output.
188
+
189
+ ``` ruby
190
+ column :name, 0, 1 do |values|
191
+ values.map(&:upcase) * ' '
192
+ end
193
+ ```
194
+
195
+ Works with one column too. Instead of getting a collection of objects, one object is passed to the block.
196
+
197
+ ``` ruby
198
+ column :first_name, 0 do |value|
199
+ value.upcase
200
+ end
201
+ ```
202
+
203
+ ### Virtual Columns
204
+
205
+ Virtual columns are not sourced from input. Omit the index to create a virtual column. Like real columns, virtual columns are included in the conformed output.
206
+
207
+ ``` ruby
208
+ column :day do
209
+ 1
210
+ end
211
+ ```
212
+
213
+ ### Inheritance
214
+
215
+ Inheriting from a schema gives access to all of the parent schema's columns.
216
+
217
+ #### Anonymous Schema
218
+
219
+ Anonymous inheritance takes inspiration from Ruby's syntax for [instantiating new classes](http://ruby-doc.org/core-1.9.3/Class.html#method-c-new).
220
+
221
+ ``` ruby
222
+ parent = Conformist.new do
223
+ column :name, 0, 1
224
+ end
225
+
226
+ child = Conformist.new parent do
227
+ column :category do
228
+ 'Child'
229
+ end
230
+ end
231
+ ```
232
+
233
+ #### Class Schema
234
+
235
+ Classical inheritance works as expected.
236
+
237
+ ``` ruby
238
+ class Parent
239
+ extend Conformist
240
+
241
+ column :name, 0, 1
242
+ end
243
+
244
+ class Child < Citizen
245
+ column :category do
246
+ 'Child'
247
+ end
248
+ end
249
+ ```
250
+
251
+ ## Upgrading from <= 0.0.3 to 0.1.0
252
+
253
+ Where previously you had
254
+
255
+ ``` ruby
256
+ class Citizen
257
+ include Conformist::Base
258
+
259
+ column :name, 0, 1
260
+ end
261
+
262
+ Citizen.load('~/file.csv').foreach do |citizen|
263
+ # ...
264
+ end
265
+ ```
266
+
267
+ You should now do
268
+
269
+ ``` ruby
270
+ require 'fastercsv'
271
+
272
+ class Citizen
273
+ extend Conformist
274
+
275
+ column :name, 0, 1
276
+ end
277
+
278
+ Citizen.conform(CSV.open('~/file.csv')).each do |citizen|
279
+ # ...
280
+ end
281
+ ```
282
+
283
+ See CHANGELOG.md for a full list of changes.
284
+
285
+ ## Compatibility and Dependancies
286
+
287
+ * MRI 1.9.2+
288
+ * MRI 1.8.7
289
+ * JRuby 1.6.5
290
+
291
+ Conformist has no explicit dependencies, although `CSV` or `FasterCSV` is commonly used.
292
+
293
+ ## Motivation
294
+
295
+ Motivation for this project came from the desire to simplify importing data from various government organisations into [Antenna Mate](http://antennamate.com). The data from each government was similar, but had completely different formatting. Some pieces of data needed preprocessing while others simply needed to be concatenated together. Not wanting to write a parser for each new government organisation, I created Conformist.
296
+
297
+ ## Copyright
298
+
299
+ Copyright © 2011 Tate Johnson. Conformist is released under the MIT license. See LICENSE for details.
data/Rakefile CHANGED
@@ -1,21 +1,9 @@
1
- begin
2
- require 'rubygems'
3
- require 'bundler'
4
- rescue LoadError
5
- raise 'Could not load the bundler gem. Install it with `gem install bundler`.'
6
- end
7
-
8
- begin
9
- Bundler.setup
10
- rescue Bundler::GemNotFound
11
- raise RuntimeError, "Bundler couldn't find some gems." +
12
- "Did you run `bundle install`?"
13
- end
14
-
15
- Bundler::GemHelper.install_tasks
16
-
1
+ require 'rubygems'
2
+ require 'bundler/gem_tasks'
3
+ require 'bundler/setup'
17
4
  require 'rake/testtask'
18
- Rake::TestTask.new(:test) do |test|
5
+
6
+ Rake::TestTask.new :test do |test|
19
7
  test.libs << 'test'
20
8
  test.pattern = 'test/**/*_test.rb'
21
9
  end
data/conformist.gemspec CHANGED
@@ -3,22 +3,30 @@ $:.push File.expand_path("../lib", __FILE__)
3
3
  require "conformist/version"
4
4
 
5
5
  Gem::Specification.new do |s|
6
- s.name = "conformist"
7
- s.version = Conformist::VERSION
8
- s.platform = Gem::Platform::RUBY
9
- s.authors = ["Tate Johnson"]
10
- s.email = ["tate@tatey.com"]
11
- s.homepage = "https://github.com/tatey/conformist"
12
- s.summary = %q{Let multiple, different input files conform to a single interface.}
13
- s.description = %q{Conformist lets you bend CSVs to your will. Let multiple, different input files conform to a single interface without rewriting your parser each time.}
6
+ s.name = "conformist"
7
+ s.version = Conformist::VERSION
8
+ s.platform = Gem::Platform::RUBY
9
+ s.authors = ["Tate Johnson"]
10
+ s.email = ["tate@tatey.com"]
11
+ s.homepage = "https://github.com/tatey/conformist"
12
+ s.summary = %q{Bend CSVs to your will.}
13
+ s.description = %q{Stop using array indexing and start using declarative schemas.}
14
+ s.post_install_message = <<-EOS
15
+ ********************************************************************************
16
+
17
+ Upgrading from <= 0.0.3? You should be aware of breaking changes. See
18
+ https://github.com/tatey/conformist and skip to "Upgrading from 0.0.3 to
19
+ 0.1.0" to learn more. Conformist will raise helpful messages where necessary.
20
+
21
+ ********************************************************************************
22
+ EOS
14
23
 
15
24
  s.rubyforge_project = "conformist"
16
25
 
17
26
  s.required_ruby_version = '>= 1.8.7'
18
27
 
19
- s.add_development_dependency 'rake', '~> 0.8.7'
20
- s.add_development_dependency 'minitest', '~> 2.1.0'
21
- s.add_dependency 'fastercsv', "~> 1.5.4"
28
+ s.add_development_dependency 'rake'
29
+ s.add_development_dependency 'minitest'
22
30
 
23
31
  s.files = `git ls-files`.split("\n")
24
32
  s.test_files = `git ls-files -- {test,spec,features}/*`.split("\n")
data/lib/conformist.rb CHANGED
@@ -1,26 +1,24 @@
1
- if RUBY_VERSION >= '1.9.0'
2
- require 'csv'
3
- else
4
- require 'fastercsv'
5
- end
6
-
7
1
  require 'conformist/base'
2
+ require 'conformist/builder'
8
3
  require 'conformist/column'
9
- require 'conformist/row'
4
+ require 'conformist/hash_struct'
5
+ require 'conformist/schema'
10
6
 
11
7
  module Conformist
12
- CSV = RUBY_VERSION >= '1.9.0' ? ::CSV : FasterCSV
8
+ unless defined? Enumerator # Compatible with 1.8
9
+ require 'generator'
10
+ Enumerator = Generator
11
+ end
12
+
13
+ def self.extended base
14
+ base.extend Schema
15
+ end
16
+
17
+ def self.foreach *args, &block
18
+ raise "`Conformist.foreach` has been removed, use something like `[MySchema1.conform(file1), MySchema2.conform(file2)].each(&block)` instead (#{caller.first})"
19
+ end
13
20
 
14
- # Enumerate over each row from multiple input files.
15
- #
16
- # Example:
17
- #
18
- # Conformist::Base.foreach Input1.load('input.csv'), Input2.load('input.csv') do |row|
19
- # Model.create! row
20
- # end
21
- #
22
- # Returns nothing.
23
- def self.foreach *bases, &block
24
- bases.each { |base| base.foreach(&block) }
21
+ def self.new *args, &block
22
+ Class.new { include Schema }.new *args, &block
25
23
  end
26
24
  end
@@ -1,52 +1,7 @@
1
1
  module Conformist
2
2
  module Base
3
3
  def self.included base
4
- base.class_eval do
5
- extend ClassMethods
6
- attr_accessor :path
7
- end
4
+ raise "`include Conformist::Base` has been removed, `extend Conformist` instead (#{caller.first})"
8
5
  end
9
-
10
- # Enumerate over each row in the input file.
11
- #
12
- # Example:
13
- #
14
- # input1 = Input1.load 'input1.csv'
15
- # input1.foreach do |row|
16
- # Model.create! row
17
- # end
18
- #
19
- # Returns nothing.
20
- def foreach &block
21
- CSV.foreach(path, self.class.options) do |row|
22
- yield Row.new(self.class.columns, row).to_hash
23
- end
24
- end
25
-
26
- module ClassMethods
27
- def column name, *indexes, &preprocessor
28
- columns << Column.new(name, *indexes, &preprocessor)
29
- end
30
-
31
- def columns
32
- @columns ||= if superclass.ancestors.include? Conformist::Base
33
- superclass.columns.dup
34
- else
35
- []
36
- end
37
- end
38
-
39
- def option value
40
- options.merge! value
41
- end
42
-
43
- def options
44
- @options ||= {}
45
- end
46
-
47
- def load path
48
- new.tap { |object| object.path = path }
49
- end
50
- end
51
- end
6
+ end
52
7
  end