sowing 1.0.0 → 1.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.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA1:
3
- metadata.gz: 7c8dbbbc138c2729840bf2b8785d4b690bee5744
4
- data.tar.gz: 61d35ff47329fe1496b5ffdd4e4ba6c1d74aa84f
3
+ metadata.gz: faf7e09c35174fbde72fba78bfc3a1dc5b8968f6
4
+ data.tar.gz: a6550d15d1c8aecac286f4ee321e43afa743776b
5
5
  SHA512:
6
- metadata.gz: f0a66a0a6fa5ebba5517136fd6341578dfe3b14b803da4c6b492bc5299b17c2f57cbae8a51243ff8e76ef2a3f369a7e81935d020b60a16e052c845cf1bb8cd45
7
- data.tar.gz: 7b62d543cc02e0a880ce32ad30cfa1caef6bfbed3e2af46646e48b3db2c89d186a6f33f23e8a61ab13569bc506318061ace081a20db79320725e30f9b55bba87
6
+ metadata.gz: 2226164bac954535578bae7ecccb005167b806e1f8b7f0e3ba63bfd1c23490053806c01b9ebfc187bd30181fec1e7a0f6ab0605c57a23450f45b3e7a6a450bf7
7
+ data.tar.gz: 037cc850febfe410c29df71d7e88180134f8c0ade5905493b8cb9ec9851cfa8a6becb2930fb1cafe0670ebb12c7986af9a79cdf259a72f2a5e50d68fff433539
data/CHANGELOG.md ADDED
@@ -0,0 +1,8 @@
1
+ ## v1.1.0
2
+
3
+ - rename: `create_or_do_nothing` to `create_or_skip`
4
+ - feature: relational data
5
+
6
+ ## v1.0.0
7
+
8
+ - feature: `create`, `create_or_do_nothing`, `create_or_update`
data/README.ja.md ADDED
@@ -0,0 +1,187 @@
1
+ # Sowing
2
+
3
+ seed データの投入、および data migration 時のデータ投入のためのライブラリ。
4
+
5
+ ## Installation
6
+
7
+ Add this line to your application's Gemfile:
8
+
9
+ ```ruby
10
+ gem 'sowing'
11
+ ```
12
+
13
+ And then execute:
14
+
15
+ $ bundle
16
+
17
+ Or install it yourself as:
18
+
19
+ $ gem install sowing
20
+
21
+ ## コンセプト
22
+
23
+ - seedに関わらずどこからでも使える
24
+ - CSV, YAML からデータを投入できる
25
+ - 投入しようとするデータが既にある場合、何もしないか更新するか選択できる
26
+ - Relational Data の投入
27
+ - ~~Pure Ruby~~ (現状はまだ Rails 依存のコードがあるが、将来的には Pure Ruby による実装にしたい)
28
+ - inspired by [sprig](https://github.com/vigetlabs/sprig)
29
+
30
+ ## 使い方
31
+
32
+ Sowing を `db/seeds.rb` で使用する例を説明します。
33
+
34
+ Directories structure example:
35
+
36
+ ```
37
+ db
38
+ ├── seeds
39
+ │   ├── books.yml
40
+ │   └── users.csv
41
+ └── seeds.rb
42
+ ```
43
+
44
+ `db/seeds/users.csv`:
45
+
46
+ ```csv
47
+ first_name,last_name
48
+ Carlotta,Wilkinson
49
+ 中平,薫
50
+ ```
51
+
52
+ `db/seeds/books.yml`:
53
+
54
+ ```yaml
55
+ item1:
56
+ name: 'Refactoring: Improving the Design of Existing Code'
57
+ author: 'Martin Fowler'
58
+ item2:
59
+ name: 'Webを支える技術'
60
+ author: '山本 陽平'
61
+ ```
62
+
63
+ 以上のファイルがあるとき `db/seeds.rb` を次のように定義します。
64
+
65
+ ```ruby
66
+ require 'sowing'
67
+
68
+ runner = Sowing::Runner.new
69
+
70
+ runner.create(User)
71
+ runner.create(Book)
72
+ ```
73
+
74
+ Sowing では `Sowing::Runner` のインスタンスを使うことでデータ投入を行います。
75
+ `#create` メソッドの引数にデータを投入したい Model を渡すと、 sowing は Model を snake cake の複数形に変換した名前で CSV か YAML ファイル を `db/seeds` 以下に探しに行きます。
76
+ 例えば `User` の場合、 `users.(csv|yaml|yml)` を探します。 (見つからない場合例外を出します)
77
+ CSV か YAML ファイルを探索するディレクトリ (data directory) は次の方法で変更できます。
78
+
79
+ ```ruby
80
+ # 方法1: Sowing::Runner.new のオプションで指定する
81
+ sowing = Sowing::Runner.new(data_directory: 'db/seeds/development')
82
+
83
+ # 方法2: configure を変更する
84
+ Sowing::Configuration.configure do |config|
85
+ config.default_data_directory = 'db/seeds/development'
86
+ end
87
+ ```
88
+
89
+ ### seeds を Rails.env によって変更する
90
+
91
+ sowing は Rails の `rake db:seed` 機能に関して何もしません。
92
+ 単にデータ投入の機能を提供するだけです。
93
+ そのため、 `Rails.env` によってシードデータとして読み込みたいデータを変更したい場合には、次のようにしてください。
94
+
95
+ Directories structure example:
96
+
97
+ ```
98
+ db
99
+ ├── seeds/
100
+ │   ├── development/
101
+ │   │   ├── books.yml
102
+ │   │   └── users.csv
103
+ │   ├── development.rb
104
+ │   ├── production/
105
+ │   ├── production.rb
106
+ │   ├── test/
107
+ │   └── test.rb
108
+ └── seeds.rb
109
+
110
+ ```
111
+
112
+ ```ruby
113
+ # db/seeds.rb
114
+ path = Rails.root.join('db', 'seeds', "#{Rails.env}.rb")
115
+ load path if path.exist?
116
+
117
+ # db/seeds/development.rb
118
+ require 'sowing'
119
+
120
+ sowing = Sowing::Runner.new(data_directory: 'db/seeds/development')
121
+
122
+ sowing.create(User)
123
+ sowing.create(Book)
124
+ ```
125
+
126
+ ## Sowing::Runner の機能
127
+
128
+ `Sowing::Runner` の instance methods には次のものがあります。
129
+
130
+ ### create(klass, filename: nil)
131
+
132
+ 引数に与えた `klass` のデータを新規作成します。
133
+ `filename` を指定しない場合、`klass` をsnake cake の複数形に変換した名前で CSV か YAML ファイル を `db/seeds` 以下に探しに行きます。(例えば、`klass` が `User` の場合、 `users.(csv|yaml|yml)` を探します。)
134
+ `filename` を指定する場合、 `new_users.csv` のように拡張子も含めて指定してください。
135
+
136
+ ### create_or_skip(klass, finding_key, filename: nil)
137
+
138
+ `#create` とほぼ同じですが、 `finding_key` で指定したカラムでDBを探索して一致するデータが見つかった場合、何もしません。
139
+ 同じスクリプトを複数回実行する可能性がある場合、 `create` より `create_or_skip` を使用することを推奨します。
140
+
141
+ 例:
142
+
143
+ ```
144
+ # 1度も下記のスクリプトを実行していないことを前提とする
145
+
146
+ runner = Sowing::Runner.new
147
+
148
+ # 1度目のデータ投入なので、users.csv からデータを投入する
149
+ runner.create_or_skip(User, :first_name)
150
+
151
+ # 上記で投入されたデータが存在するため、何もしない。
152
+ runner.create_or_skip(User, :first_name)
153
+ ```
154
+
155
+ ### create_or_update(klass, finding_key, filename: nil)
156
+
157
+ `#create_or_skip` とほぼ同じですが、`finding_key` で指定したカラムでDBを探索して一致するデータが見つかった場合、
158
+ データを更新します。
159
+
160
+ 例:
161
+
162
+ ```
163
+ # 1度も下記のスクリプトを実行していないことを前提とする
164
+
165
+ runner = Sowing::Runner.new
166
+
167
+ # 1度目のデータ投入なので、users.csv からデータを投入する
168
+ runner.create_or_skip(User, :first_name)
169
+
170
+ # users.csv と update_users.csv で同じ first_name のデータがある場合は update_users.csv の内容で上書きする
171
+ # update_users.csv にしかないデータは新規作成する
172
+ runner.create_or_skip(User, :first_name, filename: 'update_users.csv')
173
+ ```
174
+
175
+ ## Development
176
+
177
+ ### Run tests
178
+
179
+ $ bundle exec ruby test/run-test.rb
180
+
181
+ ## Contributing
182
+
183
+ Bug reports and pull requests are welcome on GitHub at https://github.com/kbaba1001/sowing.
184
+
185
+ ## License
186
+
187
+ MIT
data/README.md CHANGED
@@ -1,8 +1,6 @@
1
1
  # Sowing
2
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/sowing`. 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
3
+ seed data handling for Rails apps.
6
4
 
7
5
  ## Installation
8
6
 
@@ -22,14 +20,73 @@ Or install it yourself as:
22
20
 
23
21
  ## Usage
24
22
 
25
- TODO: Write usage instructions here
23
+ db/seeds/
24
+
25
+ ```ruby
26
+ require 'sowing'
27
+
28
+ runner = Sowing::Runner.new
29
+
30
+ # if exist db/seeds/users.(csv|yaml|yml), read data
31
+ runner.create(User)
32
+
33
+ runner.create_or_skip(User, :first_name)
34
+ runner.create_or_update(User, :first_name)
35
+
36
+ # change data root directory
37
+ sowing = Sowing::Runner.new(data_directory: Rails.root.join('db/seeds/development'))
38
+ ```
39
+
40
+ ### Relational Data
41
+
42
+ For example, the following files exist:
43
+
44
+ `users.csv`
45
+
46
+ ```csv
47
+ first_name,last_name
48
+ Carlotta,Wilkinson
49
+ 中平,薫
50
+ ```
51
+
52
+ `profiles.csv`
53
+
54
+ ```csv
55
+ user_id,address,phone
56
+ "first_name: Carlotta, last_name: Wilkinson","2001 N Clark St, Chicago, IL 60614 America","+1 111-222-3333"
57
+ "first_name: 中平, last_name: 薫","東京都新宿区新宿1-1-1","090-1111-2222"
58
+ ```
59
+
60
+ And, the ruby code using sowing:
61
+
62
+ ```ruby
63
+ require 'sowing'
64
+
65
+ runner = Sowing::Runner.new
66
+
67
+ runner.create(User)
68
+ runner.create(Profile) do
69
+ mapping :user_id do |cel|
70
+ # cel #=> {'first_name' => 'Carlotta', 'last_name' => 'Wilkinson'}
71
+
72
+ User.find_by(
73
+ first_name: cel['first_name'],
74
+ last_name: cel['last_name']
75
+ ).id # Must be return user id
76
+ end
77
+ end
78
+ ```
26
79
 
27
80
  ## Development
28
81
 
29
82
  ### Run tests
30
83
 
31
- $ bundle exec rake
84
+ $ bundle exec ruby test/run-test.rb
32
85
 
33
86
  ## Contributing
34
87
 
35
- Bug reports and pull requests are welcome on GitHub at https://github.com/[USERNAME]/sowing.
88
+ Bug reports and pull requests are welcome on GitHub at https://github.com/kbaba1001/sowing.
89
+
90
+ ## License
91
+
92
+ MIT
data/lib/sowing.rb CHANGED
@@ -1,4 +1,6 @@
1
1
  module Sowing
2
+ class DataFileNotFound < StandardError; end
3
+ class StrategyNotFound < StandardError; end
2
4
  end
3
5
 
4
6
  require 'pathname'
@@ -8,4 +10,6 @@ require_relative 'sowing/strategies'
8
10
  require_relative 'sowing/version'
9
11
 
10
12
  require_relative 'sowing/configuration'
13
+ require_relative 'sowing/definition_proxy'
14
+ require_relative 'sowing/selector'
11
15
  require_relative 'sowing/runner'
@@ -0,0 +1,11 @@
1
+ class Sowing::DefinitionProxy
2
+ attr_reader :mappings
3
+
4
+ def initialize
5
+ @mappings = {}
6
+ end
7
+
8
+ def mapping(attr, &block)
9
+ @mappings[attr.to_s] = block
10
+ end
11
+ end
data/lib/sowing/runner.rb CHANGED
@@ -3,57 +3,50 @@ class Sowing::Runner
3
3
 
4
4
  def initialize(data_directory: nil)
5
5
  @data_directory = Pathname(data_directory || Sowing::Configuration.config.default_data_directory)
6
+ @selector = Sowing::Selector.new(@data_directory)
6
7
  end
7
8
 
8
- def create(klass, filename: nil)
9
- find_file(klass, filename: filename) do |file, strategy|
10
- strategy.create(klass, file)
9
+ def create(klass, filename: nil, &block)
10
+ file, strategy = @selector.find(klass, filename)
11
+
12
+ create_rows(file, strategy, &block).each do |row|
13
+ strategy.create(klass, row)
11
14
  end
12
15
  end
13
16
 
14
- def create_or_do_nothing(klass, finding_key, filename: nil)
15
- find_file(klass, filename: filename) do |file, strategy|
16
- strategy.create_or_do_nothing(klass, file, finding_key)
17
+ def create_or_skip(klass, finding_key, filename: nil, &block)
18
+ file, strategy = @selector.find(klass, filename)
19
+
20
+ create_rows(file, strategy, &block).each do |row|
21
+ strategy.create_or_skip(klass, row, finding_key)
17
22
  end
18
23
  end
19
24
 
20
- def create_or_update(klass, finding_key, filename: nil)
21
- find_file(klass, filename: filename) do |file, strategy|
22
- strategy.create_or_update(klass, file, finding_key)
25
+ def create_or_update(klass, finding_key, filename: nil, &block)
26
+ file, strategy = @selector.find(klass, filename)
27
+
28
+ create_rows(file, strategy, &block).each do |row|
29
+ strategy.create_or_update(klass, row, finding_key)
23
30
  end
24
31
  end
25
32
 
26
33
  private
27
34
 
28
- def find_file(klass, filename: nil)
29
- if filename
30
- file = data_directory.join(filename)
31
- if file.exist?
32
- yield(file, select_strategy(file.extname[1..-1]))
33
- else
34
- raise "not found: #{file}"
35
- end
36
-
37
- return
38
- end
35
+ def create_rows(file, strategy, &block)
36
+ proxy = Sowing::DefinitionProxy.new
37
+ proxy.instance_exec(&block) if block_given?
39
38
 
40
- pathname = data_directory.join(klass.to_s.underscore.pluralize)
41
- ext = Sowing::Configuration.config.extensions.find {|ext| Pathname("#{pathname}.#{ext}").exist? }
39
+ strategy.read_data(file).map {|row|
40
+ proxy.mappings.each do |key, pproc|
41
+ # TODO if i can skip, i want to skip. Doing so will increase performance
42
+ row[key.to_s] = pproc[string_to_hash(row.fetch(key.to_s))]
43
+ end
42
44
 
43
- if ext
44
- yield(Pathname("#{pathname}.#{ext}"), select_strategy(ext))
45
- else
46
- raise "not found: #{pathanme}.(#{Sowing::Configuration.config.extensions.join('|')})"
47
- end
45
+ row
46
+ }
48
47
  end
49
48
 
50
- def select_strategy(ext)
51
- strategy_klass = Sowing::Configuration.config.strategies[ext]
52
-
53
- if strategy_klass
54
- strategy_klass.new
55
- else
56
- raise "strategy not found: extension #{ext}"
57
- end
49
+ def string_to_hash(str)
50
+ str.delete(' ').split(/[:,]/).each_slice(2).to_h
58
51
  end
59
52
  end
@@ -0,0 +1,45 @@
1
+ class Sowing::Selector
2
+ attr_reader :data_directory
3
+
4
+ def initialize(data_directory)
5
+ @data_directory = data_directory
6
+ end
7
+
8
+ # @return [Array<Pathname, Sowing::Strategies>]
9
+ def find(klass, filename = nil)
10
+ if filename
11
+ find_file_from_filename(filename)
12
+ else
13
+ file_file_from_convention(klass)
14
+ end
15
+ end
16
+
17
+ private
18
+
19
+ def find_file_from_filename(filename)
20
+ file = data_directory.join(filename)
21
+
22
+ raise DataFileNotFound.new("not found: #{file}") unless file.exist?
23
+
24
+ [file, select_strategy(file.extname[1..-1])]
25
+ end
26
+
27
+ def file_file_from_convention(klass)
28
+ pathname = data_directory.join(klass.to_s.underscore.pluralize)
29
+ ext = Sowing::Configuration.config.extensions.find {|ext| Pathname("#{pathname}.#{ext}").exist? }
30
+
31
+ raise DataFileNotFound.new("not found: #{pathanme}.(#{Sowing::Configuration.config.extensions.join('|')})") unless ext
32
+
33
+ [Pathname("#{pathname}.#{ext}"), select_strategy(ext)]
34
+ end
35
+
36
+ def select_strategy(ext)
37
+ strategy_klass = Sowing::Configuration.config.strategies[ext]
38
+
39
+ if strategy_klass
40
+ strategy_klass.new
41
+ else
42
+ raise StrategyNotFound.new("strategy not found: extension #{ext}")
43
+ end
44
+ end
45
+ end
@@ -1,21 +1,28 @@
1
1
  module Sowing
2
2
  module Strategies
3
3
  class AbstractStrategy
4
- def create(klass, csv_filename)
4
+ def create(klass, row)
5
5
  raise NotImplementedError
6
6
  end
7
7
 
8
- def create_or_do_nothing(klass, csv_filename, finding_key)
8
+ def create_or_skip(klass, row, finding_key)
9
9
  raise NotImplementedError
10
10
  end
11
11
 
12
- def create_or_update(klass, csv_filename, finding_key)
12
+ def create_or_update(klass, row, finding_key)
13
+ raise NotImplementedError
14
+ end
15
+
16
+ # @return [Enumerable] object
17
+ def read_data(filename)
13
18
  raise NotImplementedError
14
19
  end
15
20
 
16
21
  private
17
22
 
18
23
  def print_object_info(object)
24
+ return if ENV['SOWING_QUIET']
25
+
19
26
  print 'create: '
20
27
  p object
21
28
  end
@@ -1,37 +1,25 @@
1
1
  module Sowing
2
2
  module Strategies
3
3
  class ActiveRecordAbstract < AbstractStrategy
4
- def create(klass, filename)
5
- read_data(filename).each do |row|
6
- object = klass.create!(row.to_hash)
4
+ def create(klass, row)
5
+ object = klass.create!(row.to_hash)
7
6
 
8
- print_object_info(object)
9
- end
10
- end
11
-
12
- def create_or_do_nothing(klass, filename, finding_key)
13
- read_data(filename).each do |row|
14
- klass.find_or_initialize_by(finding_key => row[finding_key.to_s]) do |object|
15
- object.update!(row.to_hash)
16
-
17
- print_object_info(object)
18
- end
19
- end
7
+ print_object_info(object)
20
8
  end
21
9
 
22
- def create_or_update(klass, filename, finding_key)
23
- read_data(filename).each do |row|
24
- object = klass.find_or_initialize_by(finding_key => row[finding_key.to_s])
10
+ def create_or_skip(klass, row, finding_key)
11
+ klass.find_or_initialize_by(finding_key => row[finding_key.to_s]) do |object|
25
12
  object.update!(row.to_hash)
26
13
 
27
14
  print_object_info(object)
28
15
  end
29
16
  end
30
17
 
31
- private
18
+ def create_or_update(klass, row, finding_key)
19
+ object = klass.find_or_initialize_by(finding_key => row[finding_key.to_s])
20
+ object.update!(row.to_hash)
32
21
 
33
- def read_data(filename)
34
- raise NotImplementedError
22
+ print_object_info(object)
35
23
  end
36
24
  end
37
25
  end
@@ -3,8 +3,6 @@ require 'csv'
3
3
  module Sowing
4
4
  module Strategies
5
5
  class ActiveRecordCsv < ActiveRecordAbstract
6
- private
7
-
8
6
  def read_data(csv_filename)
9
7
  CSV.read(csv_filename, headers: true)
10
8
  end
@@ -3,8 +3,6 @@ require 'yaml'
3
3
  module Sowing
4
4
  module Strategies
5
5
  class ActiveRecordYaml < ActiveRecordAbstract
6
- private
7
-
8
6
  def read_data(yaml_filename)
9
7
  YAML.load_file(yaml_filename).values
10
8
  end
@@ -1,3 +1,3 @@
1
1
  module Sowing
2
- VERSION = "1.0.0"
2
+ VERSION = "1.1.0"
3
3
  end
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: sowing
3
3
  version: !ruby/object:Gem::Version
4
- version: 1.0.0
4
+ version: 1.1.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - kbaba1001
8
8
  autorequire:
9
9
  bindir: exe
10
10
  cert_chain: []
11
- date: 2017-07-15 00:00:00.000000000 Z
11
+ date: 2017-08-30 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: activerecord
@@ -144,14 +144,18 @@ extensions: []
144
144
  extra_rdoc_files: []
145
145
  files:
146
146
  - ".gitignore"
147
+ - CHANGELOG.md
147
148
  - Gemfile
149
+ - README.ja.md
148
150
  - README.md
149
151
  - Rakefile
150
152
  - bin/console
151
153
  - bin/setup
152
154
  - lib/sowing.rb
153
155
  - lib/sowing/configuration.rb
156
+ - lib/sowing/definition_proxy.rb
154
157
  - lib/sowing/runner.rb
158
+ - lib/sowing/selector.rb
155
159
  - lib/sowing/strategies.rb
156
160
  - lib/sowing/strategies/abstract_strategy.rb
157
161
  - lib/sowing/strategies/active_record_abstract.rb