sowing 1.0.0 → 1.1.0

Sign up to get free protection for your applications and to get access to all the features.
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