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.
- data/{CHANGELOG.markdown → CHANGELOG.md} +10 -0
- data/README.md +299 -0
- data/Rakefile +5 -17
- data/conformist.gemspec +19 -11
- data/lib/conformist.rb +17 -19
- data/lib/conformist/base.rb +2 -47
- data/lib/conformist/builder.rb +10 -0
- data/lib/conformist/column.rb +14 -17
- data/lib/conformist/hash_struct.rb +33 -0
- data/lib/conformist/schema.rb +66 -0
- data/lib/conformist/version.rb +1 -1
- data/test/helper.rb +2 -11
- data/test/schemas/acma.rb +15 -0
- data/test/{definitions → schemas}/fcc.rb +2 -4
- data/test/unit/conformist/base_test.rb +9 -0
- data/test/unit/conformist/builder_test.rb +12 -0
- data/test/{conformist → unit/conformist}/column_test.rb +4 -0
- data/test/unit/conformist/hash_struct_test.rb +48 -0
- data/test/unit/conformist/schema_test.rb +74 -0
- data/test/unit/conformist_test.rb +20 -0
- data/test/unit/integration_test.rb +72 -0
- metadata +66 -74
- data/README.markdown +0 -157
- data/lib/conformist/row.rb +0 -26
- data/test/conformist/base_test.rb +0 -49
- data/test/conformist/conformist_test.rb +0 -21
- data/test/conformist/row_test.rb +0 -21
- data/test/definitions/acma.rb +0 -24
@@ -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
|
+

|
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
|
-
|
2
|
-
|
3
|
-
|
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
|
-
|
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
|
7
|
-
s.version
|
8
|
-
s.platform
|
9
|
-
s.authors
|
10
|
-
s.email
|
11
|
-
s.homepage
|
12
|
-
s.summary
|
13
|
-
s.description
|
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'
|
20
|
-
s.add_development_dependency 'minitest'
|
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/
|
4
|
+
require 'conformist/hash_struct'
|
5
|
+
require 'conformist/schema'
|
10
6
|
|
11
7
|
module Conformist
|
12
|
-
|
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
|
-
|
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
|
data/lib/conformist/base.rb
CHANGED
@@ -1,52 +1,7 @@
|
|
1
1
|
module Conformist
|
2
2
|
module Base
|
3
3
|
def self.included base
|
4
|
-
|
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
|