potpourri 0.1.0 → 0.2.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.
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.