conformist 0.0.3 → 0.1.0

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.
@@ -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