potpourri 0.1.0 → 0.2.0

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml CHANGED
@@ -1,15 +1,7 @@
1
1
  ---
2
- !binary "U0hBMQ==":
3
- metadata.gz: !binary |-
4
- ZDZlMjkzYzYzOTdkY2E2ZDNkZTVmMDlhZDFhNGY1MDRiOTgzYzQwMA==
5
- data.tar.gz: !binary |-
6
- ODU5NjhiMTk1ZGNmODdiOWI2ZGU3ZjNkZjc2ZGRhYjNkZWM2Y2YzOQ==
2
+ SHA1:
3
+ metadata.gz: 967a37b13ab8e26fda50157cb6fc92d1665e7dcd
4
+ data.tar.gz: 0bf7c3a2c6a2a1603a171b86044ec4da19b8fd06
7
5
  SHA512:
8
- metadata.gz: !binary |-
9
- ZWI0YjhhZjcwNjk4ZmE0YzllZGYxYjYzMzhjM2FlYmM3NGQzOWM5MjEwMWNh
10
- NGU2MWJhNDhhOGJlZTlhZmZiMzdkNTg0YWMzYzNlMWVhY2JhZGRmNGFkNGM5
11
- MjZmYzY0ZjA3Zjk0ZDhlYmYwMzc1OGZiZTliYTg1MzUxNTFhNjU=
12
- data.tar.gz: !binary |-
13
- MmRmNjhmMmE5NWI2NmVmYThhY2YwMWQ5OWE4ZmE3NDJhNjBmMzc2Mzk4Mzk3
14
- NGRiYzU3Nzg2OWQ5YTk3MDYxN2RkMzhmZWE1OGIyNDMxODdkZDgxODllNGVi
15
- ZTE2Mzk0NTljZWY4YTI0NzFkZTNjODdjNDcwOTc0MGU3ZjM1MjE=
6
+ metadata.gz: 7f1449404d6a6f59bc162151835488c06a8900fcc81d8753b46cb70719a38ba374243376e4649516684b62a253c3e24fec92abab5237d10a83a2380fe3415d44
7
+ data.tar.gz: 167ea8d0944bf69d4d5b4a4775639e284ebee459195df3854b6c65bde3edd2b58d2359693ac8b05f97b9398d77b34e9acee7bdf5b9e4be537dfe79ab39f10527
data/README.md CHANGED
@@ -1,8 +1,5 @@
1
1
  # Potpourri
2
-
3
- Welcome to your new gem! In this directory, you'll find the files you need to be able to package up your Ruby library into a gem. Put your Ruby code in the file `lib/potpourri`. To experiment with that code, run `bin/console` for an interactive prompt.
4
-
5
- TODO: Delete this and the text above, and describe your gem
2
+ A simple DSL to structure CSV importer and exporter for all of your models.
6
3
 
7
4
  ## Installation
8
5
 
@@ -22,7 +19,121 @@ Or install it yourself as:
22
19
 
23
20
  ## Usage
24
21
 
25
- TODO: Write usage instructions here
22
+ The basic implementation will work for models which accept a hash as the only argument. Start by defining a report class which inherits from `Potpourri::Report`.
23
+
24
+ ```ruby
25
+ class Starship
26
+ attr_accessor :captain, :registration_number, :shield_frequency
27
+
28
+ def initialize(captain: nil, registration_number: nil, shield_frequency: nil)
29
+ captain, registation_number, shield_frequency = [captain, registation_number, shield_frequency]
30
+ end
31
+ end
32
+
33
+ class StarshipReport < Potpourri::Report
34
+ resource_class Starship
35
+
36
+ fields [
37
+ Potpourri::Field(:captain),
38
+ Potpourri::ExportableField(:registration_number),
39
+ Potpourri::ImportableField(:shield_frequency)
40
+ ]
41
+ end
42
+ ```
43
+
44
+ Thats it! Youll now be able to initialize a new Starship report which takes a path as its only argument:
45
+
46
+ ```ruby
47
+ report = StarshipReport.new('starfleet/records/starships.csv')
48
+
49
+ report.export(starships)
50
+ # => a CSV object
51
+ # Captain,Registration Number
52
+ # Wharf, NX-74205
53
+
54
+ # Given a csv at the specified path
55
+ # Captain, Shield Frequency
56
+ # Sisko, 3.14159
57
+
58
+ report.import # => An array of Starship objects
59
+ ```
60
+
61
+ ### Customizing Fields
62
+
63
+ Fields can be customized in a number of ways. It is easy to decide what fields will be imported and which are
64
+ exported. Extra fields in an import will simply be ignored, so all generated exports are able to be imported.
65
+
66
+ - Potpourri::Field => These fields will be both imported and exported
67
+ - Potpourri::ExportableField => These fields will only be exported
68
+ - Potpourri::ImportableField => These fields will only be imported
69
+
70
+ Fields will interpolate which methods are used to get and set values on a model. They will also use a
71
+ variation of the Rails `#titleize` method to generate a header for the csv.
72
+
73
+ `Potpourri::Field.new(:captain)` use `captain` as a getter and `captain=` as a setter.
74
+
75
+ These assumptions can be easily overriden by passing some options into the Field when the report is defined.
76
+
77
+ ```ruby
78
+ fields [
79
+ Potpourri::Field(:registration_number),
80
+ Potpourri::Field(:captain),
81
+ Potpourri::ImportableField(:shield_frequency)
82
+ ]
83
+ ...
84
+
85
+ fields [
86
+ Potpourri::Field(
87
+ :captain,
88
+ export_method: :command_officer,
89
+ import_method: :fearless_leader=,
90
+ header: 'Designation'
91
+ )
92
+ ]
93
+
94
+ ...
95
+
96
+ ```
97
+ The importable and exportable fields also respond to the same methods, however of course they will only be either importable or exportable.
98
+
99
+ ### ActiveRecord Extension
100
+
101
+ Importing and exporting dynamic models is only so useful. Enter the ActiveRecord extension. It provides a
102
+ `Potpourri::ActiveRecord::Report` class. This class is a subclass of `Potpourri::Report` so it responds in
103
+ much the same way, with some bells and whistles.
104
+
105
+ ```ruby
106
+ class StarshipReport < Potpourri::ActiveRecord::Report
107
+ resource_class Starship
108
+
109
+ fields [
110
+ Potpourri::IdentifierField(:registration_number),
111
+ Potpourri::Field(:captain),
112
+ Potpourri::ImportableField(:shield_frequency)
113
+ ]
114
+
115
+ can_create_new_records
116
+ can_update_existing_records
117
+ end
118
+ ```
119
+
120
+ The big additions are:
121
+ - The identifierField
122
+ - This field is required to import any record
123
+ - it can be any model field so long as it is unique
124
+ - it accepts all the same options as `Potpourri::Field`
125
+ - Is not importable
126
+
127
+ - `can_create_new_records`
128
+ - lets the report create records if no match for the identifier can be found
129
+ - false by default
130
+ - accepts an argument and can be dynamically changed
131
+
132
+ - `can_update_existing_fields`
133
+ - lets the report update existing records
134
+ - will not overwrite the identifier field
135
+ - false by default
136
+ - accepts an argument and can be dynamically changed
26
137
 
27
138
  ## Development
28
139
 
data/Rakefile CHANGED
@@ -1 +1,7 @@
1
1
  require "bundler/gem_tasks"
2
+ require 'rspec/core/rake_task'
3
+
4
+ RSpec::Core::RakeTask.new
5
+
6
+ task :default => :spec
7
+ task :test => :spec
@@ -0,0 +1,62 @@
1
+ module Potpourri
2
+ module ActiveRecordExtension
3
+ def self.included(mod)
4
+ raise NotAReport unless mod.ancestors.include?(Report)
5
+ mod.extend ClassMethods
6
+ end
7
+
8
+ class MissingIdentifierField < StandardError; end
9
+ class RecordNotFound < StandardError; end
10
+
11
+ def can_update_existing_records?
12
+ self.class.instance_variable_get '@can_update_existing_records'
13
+ end
14
+
15
+ def can_create_new_records?
16
+ self.class.instance_variable_get '@can_create_new_records'
17
+ end
18
+
19
+
20
+ def identifier_field
21
+ fields.detect &:identifier?
22
+ end
23
+
24
+ def import
25
+ resources = super
26
+ resources.each { |resource| resource.save! }
27
+ end
28
+
29
+ private
30
+
31
+ def fetch_resource(row)
32
+ raise MissingIdentifierField unless importable?
33
+
34
+ return find_record(row) unless can_create_new_records?
35
+ return resource_class.new unless can_update_existing_records?
36
+
37
+ resource = resource_class.find_by(id_params row) || resource_class.new
38
+ end
39
+
40
+ def find_record(row)
41
+ resource = resource_class.find_by! id_params(row)
42
+ end
43
+
44
+ def importable?
45
+ (can_create_new_records? || can_update_existing_records?) && identifier_field
46
+ end
47
+
48
+ def id_params(row)
49
+ Hash[identifier_field.export_method, row[identifier_field.header]]
50
+ end
51
+
52
+ module ClassMethods
53
+ def create_new_records(flag = true)
54
+ @can_create_new_records = flag
55
+ end
56
+
57
+ def update_existing_records(flag = true)
58
+ @can_update_existing_records = flag
59
+ end
60
+ end
61
+ end
62
+ end
@@ -0,0 +1,13 @@
1
+ module Potpourri
2
+ module ActiveRecord
3
+ class IdentifierField < Field
4
+ def importable?
5
+ false
6
+ end
7
+
8
+ def identifier?
9
+ true
10
+ end
11
+ end
12
+ end
13
+ end
@@ -0,0 +1,25 @@
1
+ module Potpourri
2
+ module ReportConfigs
3
+ def self.included(mod)
4
+ mod.extend ClassMethods
5
+ end
6
+
7
+ def fields
8
+ self.class.instance_variable_get '@fields'
9
+ end
10
+
11
+ def resource_class
12
+ self.class.instance_variable_get '@klass'
13
+ end
14
+
15
+ module ClassMethods
16
+ def fields(fields)
17
+ @fields = fields
18
+ end
19
+
20
+ def resource_class(klass)
21
+ @klass = klass
22
+ end
23
+ end
24
+ end
25
+ end
@@ -0,0 +1,7 @@
1
+ module Potpourri
2
+ module Titleize
3
+ def titleize(str)
4
+ str.to_s.split(/ |\_/).map(&:capitalize).join(" ")
5
+ end
6
+ end
7
+ end
@@ -0,0 +1,7 @@
1
+ module Potpourri
2
+ class ExportableField < Field
3
+ def importable?
4
+ false
5
+ end
6
+ end
7
+ end
@@ -0,0 +1,35 @@
1
+ module Potpourri
2
+ class Field
3
+ include Potpourri::Titleize
4
+
5
+ class Unimportable < StandardError; end
6
+ class Unexportable < StandardError; end
7
+
8
+ attr_reader :name, :header, :export_method, :import_method
9
+
10
+ def initialize(name, options = {})
11
+ @export_method = options[:export_method] || name.to_sym
12
+ @import_method = options[:import_method] || "#{ name }=".to_sym
13
+ @header = options[:header] || titleize(name)
14
+ @name = name.to_sym
15
+ end
16
+
17
+ def importable?
18
+ true
19
+ end
20
+
21
+ def exportable?
22
+ true
23
+ end
24
+
25
+ def import(resource, value)
26
+ raise Unimportable unless importable?
27
+ resource.public_send import_method, value
28
+ end
29
+
30
+ def export(resource)
31
+ raise Unexportable unless exportable?
32
+ resource.public_send export_method
33
+ end
34
+ end
35
+ end
@@ -0,0 +1,7 @@
1
+ module Potpourri
2
+ class ImportableField < Field
3
+ def exportable?
4
+ false
5
+ end
6
+ end
7
+ end
@@ -0,0 +1,55 @@
1
+ require 'csv'
2
+
3
+ module Potpourri
4
+ class Report
5
+ include ReportConfigs
6
+
7
+ attr_reader :path
8
+
9
+ def initialize(path)
10
+ @path = path
11
+ end
12
+
13
+ def headers
14
+ fields.map &:header
15
+ end
16
+
17
+ def import
18
+ CSV.read(path, headers: true, converters: :numeric).map do |row|
19
+ resource = fetch_resource(row)
20
+
21
+ importable_fields.each do |field|
22
+ field.import resource, row[field.header]
23
+ end
24
+
25
+ resource
26
+ end
27
+ end
28
+
29
+ def export(collection)
30
+ CSV.open(path, 'w') do |csv|
31
+ csv << headers
32
+
33
+ collection.each do |item|
34
+ csv << exportable_fields.map { |field| field.export item }
35
+ end
36
+
37
+ csv
38
+ end
39
+ end
40
+
41
+ private
42
+
43
+ def importable_fields
44
+ fields.select &:importable?
45
+ end
46
+
47
+ def exportable_fields
48
+ fields.select &:exportable?
49
+ end
50
+
51
+ def fetch_resource(row)
52
+ resource_class.new
53
+ end
54
+ end
55
+ end
@@ -1,5 +1,9 @@
1
1
  require "potpourri/version"
2
2
 
3
+ ROOT = File.expand_path '../', __FILE__
4
+
3
5
  module Potpourri
4
- # Your code goes here...
6
+ Dir.glob(File.join ROOT, 'lib/**/*.rb').each { |f| require f }
7
+ Dir.glob(File.join ROOT, 'models/**/*.rb').each { |f| require f }
8
+ Dir.glob(File.join ROOT, 'extensions/**/*.rb').each { |f| require f }
5
9
  end
@@ -1,3 +1,3 @@
1
1
  module Potpourri
2
- VERSION = "0.1.0"
2
+ VERSION = "0.2.0"
3
3
  end
@@ -10,7 +10,7 @@ Gem::Specification.new do |spec|
10
10
  spec.email = ["brendan.g.deere@gmail.com"]
11
11
 
12
12
  spec.summary = %q{Because you're tired of writting csv importer and exporters.}
13
- spec.description = %q{A simple DSL to structure CSV importer and exporters for all of your models.}
13
+ spec.description = %q{A simple DSL to structure CSV importer and exporter for all of your models.}
14
14
  spec.homepage = "https://github.com/brendandeere/potpourri"
15
15
 
16
16
  spec.files = `git ls-files -z`.split("\x0").reject { |f| f.match(%r{^(test|spec|features)/}) }
@@ -21,6 +21,12 @@ Gem::Specification.new do |spec|
21
21
  if spec.respond_to?(:metadata)
22
22
  end
23
23
 
24
+ spec.add_development_dependency "activerecord"
25
+ spec.add_development_dependency "sqlite3"
24
26
  spec.add_development_dependency "bundler", "~> 1.9"
25
27
  spec.add_development_dependency "rake", "~> 10.0"
28
+ spec.add_development_dependency "pry"
29
+ spec.add_development_dependency "rspec"
30
+ spec.add_development_dependency "simplecov"
31
+ spec.add_development_dependency "database_cleaner"
26
32
  end
metadata CHANGED
@@ -1,59 +1,150 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: potpourri
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.1.0
4
+ version: 0.2.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - brendandeere
8
8
  autorequire:
9
9
  bindir: exe
10
10
  cert_chain: []
11
- date: 2015-11-11 00:00:00.000000000 Z
11
+ date: 2015-11-25 00:00:00.000000000 Z
12
12
  dependencies:
13
+ - !ruby/object:Gem::Dependency
14
+ name: activerecord
15
+ requirement: !ruby/object:Gem::Requirement
16
+ requirements:
17
+ - - ">="
18
+ - !ruby/object:Gem::Version
19
+ version: '0'
20
+ type: :development
21
+ prerelease: false
22
+ version_requirements: !ruby/object:Gem::Requirement
23
+ requirements:
24
+ - - ">="
25
+ - !ruby/object:Gem::Version
26
+ version: '0'
27
+ - !ruby/object:Gem::Dependency
28
+ name: sqlite3
29
+ requirement: !ruby/object:Gem::Requirement
30
+ requirements:
31
+ - - ">="
32
+ - !ruby/object:Gem::Version
33
+ version: '0'
34
+ type: :development
35
+ prerelease: false
36
+ version_requirements: !ruby/object:Gem::Requirement
37
+ requirements:
38
+ - - ">="
39
+ - !ruby/object:Gem::Version
40
+ version: '0'
13
41
  - !ruby/object:Gem::Dependency
14
42
  name: bundler
15
43
  requirement: !ruby/object:Gem::Requirement
16
44
  requirements:
17
- - - ~>
45
+ - - "~>"
18
46
  - !ruby/object:Gem::Version
19
47
  version: '1.9'
20
48
  type: :development
21
49
  prerelease: false
22
50
  version_requirements: !ruby/object:Gem::Requirement
23
51
  requirements:
24
- - - ~>
52
+ - - "~>"
25
53
  - !ruby/object:Gem::Version
26
54
  version: '1.9'
27
55
  - !ruby/object:Gem::Dependency
28
56
  name: rake
29
57
  requirement: !ruby/object:Gem::Requirement
30
58
  requirements:
31
- - - ~>
59
+ - - "~>"
32
60
  - !ruby/object:Gem::Version
33
61
  version: '10.0'
34
62
  type: :development
35
63
  prerelease: false
36
64
  version_requirements: !ruby/object:Gem::Requirement
37
65
  requirements:
38
- - - ~>
66
+ - - "~>"
39
67
  - !ruby/object:Gem::Version
40
68
  version: '10.0'
41
- description: A simple DSL to structure CSV importer and exporters for all of your
42
- models.
69
+ - !ruby/object:Gem::Dependency
70
+ name: pry
71
+ requirement: !ruby/object:Gem::Requirement
72
+ requirements:
73
+ - - ">="
74
+ - !ruby/object:Gem::Version
75
+ version: '0'
76
+ type: :development
77
+ prerelease: false
78
+ version_requirements: !ruby/object:Gem::Requirement
79
+ requirements:
80
+ - - ">="
81
+ - !ruby/object:Gem::Version
82
+ version: '0'
83
+ - !ruby/object:Gem::Dependency
84
+ name: rspec
85
+ requirement: !ruby/object:Gem::Requirement
86
+ requirements:
87
+ - - ">="
88
+ - !ruby/object:Gem::Version
89
+ version: '0'
90
+ type: :development
91
+ prerelease: false
92
+ version_requirements: !ruby/object:Gem::Requirement
93
+ requirements:
94
+ - - ">="
95
+ - !ruby/object:Gem::Version
96
+ version: '0'
97
+ - !ruby/object:Gem::Dependency
98
+ name: simplecov
99
+ requirement: !ruby/object:Gem::Requirement
100
+ requirements:
101
+ - - ">="
102
+ - !ruby/object:Gem::Version
103
+ version: '0'
104
+ type: :development
105
+ prerelease: false
106
+ version_requirements: !ruby/object:Gem::Requirement
107
+ requirements:
108
+ - - ">="
109
+ - !ruby/object:Gem::Version
110
+ version: '0'
111
+ - !ruby/object:Gem::Dependency
112
+ name: database_cleaner
113
+ requirement: !ruby/object:Gem::Requirement
114
+ requirements:
115
+ - - ">="
116
+ - !ruby/object:Gem::Version
117
+ version: '0'
118
+ type: :development
119
+ prerelease: false
120
+ version_requirements: !ruby/object:Gem::Requirement
121
+ requirements:
122
+ - - ">="
123
+ - !ruby/object:Gem::Version
124
+ version: '0'
125
+ description: A simple DSL to structure CSV importer and exporter for all of your models.
43
126
  email:
44
127
  - brendan.g.deere@gmail.com
45
128
  executables: []
46
129
  extensions: []
47
130
  extra_rdoc_files: []
48
131
  files:
49
- - .gitignore
50
- - .rspec
51
- - .travis.yml
132
+ - ".gitignore"
133
+ - ".rspec"
134
+ - ".travis.yml"
52
135
  - Gemfile
53
136
  - README.md
54
137
  - Rakefile
55
138
  - bin/console
56
139
  - bin/setup
140
+ - lib/extensions/active_record/active_record_extension.rb
141
+ - lib/extensions/active_record/indentifier_field.rb
142
+ - lib/lib/report_configs.rb
143
+ - lib/lib/titleize.rb
144
+ - lib/models/exportable_field.rb
145
+ - lib/models/field.rb
146
+ - lib/models/importable_field.rb
147
+ - lib/models/report.rb
57
148
  - lib/potpourri.rb
58
149
  - lib/potpourri/version.rb
59
150
  - potpourri.gemspec
@@ -66,17 +157,17 @@ require_paths:
66
157
  - lib
67
158
  required_ruby_version: !ruby/object:Gem::Requirement
68
159
  requirements:
69
- - - ! '>='
160
+ - - ">="
70
161
  - !ruby/object:Gem::Version
71
162
  version: '0'
72
163
  required_rubygems_version: !ruby/object:Gem::Requirement
73
164
  requirements:
74
- - - ! '>='
165
+ - - ">="
75
166
  - !ruby/object:Gem::Version
76
167
  version: '0'
77
168
  requirements: []
78
169
  rubyforge_project:
79
- rubygems_version: 2.4.8
170
+ rubygems_version: 2.2.3
80
171
  signing_key:
81
172
  specification_version: 4
82
173
  summary: Because you're tired of writting csv importer and exporters.