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 +4 -4
- data/CHANGELOG.md +8 -0
- data/README.ja.md +187 -0
- data/README.md +63 -6
- data/lib/sowing.rb +4 -0
- data/lib/sowing/definition_proxy.rb +11 -0
- data/lib/sowing/runner.rb +28 -35
- data/lib/sowing/selector.rb +45 -0
- data/lib/sowing/strategies/abstract_strategy.rb +10 -3
- data/lib/sowing/strategies/active_record_abstract.rb +9 -21
- data/lib/sowing/strategies/active_record_csv.rb +0 -2
- data/lib/sowing/strategies/active_record_yaml.rb +0 -2
- data/lib/sowing/version.rb +1 -1
- metadata +6 -2
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA1:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: faf7e09c35174fbde72fba78bfc3a1dc5b8968f6
|
4
|
+
data.tar.gz: a6550d15d1c8aecac286f4ee321e43afa743776b
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 2226164bac954535578bae7ecccb005167b806e1f8b7f0e3ba63bfd1c23490053806c01b9ebfc187bd30181fec1e7a0f6ab0605c57a23450f45b3e7a6a450bf7
|
7
|
+
data.tar.gz: 037cc850febfe410c29df71d7e88180134f8c0ade5905493b8cb9ec9851cfa8a6becb2930fb1cafe0670ebb12c7986af9a79cdf259a72f2a5e50d68fff433539
|
data/CHANGELOG.md
ADDED
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
|
-
|
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
|
-
|
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
|
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/
|
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'
|
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
|
-
|
10
|
-
|
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
|
15
|
-
|
16
|
-
|
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
|
-
|
22
|
-
|
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
|
29
|
-
|
30
|
-
|
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
|
-
|
41
|
-
|
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
|
-
|
44
|
-
|
45
|
-
else
|
46
|
-
raise "not found: #{pathanme}.(#{Sowing::Configuration.config.extensions.join('|')})"
|
47
|
-
end
|
45
|
+
row
|
46
|
+
}
|
48
47
|
end
|
49
48
|
|
50
|
-
def
|
51
|
-
|
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,
|
4
|
+
def create(klass, row)
|
5
5
|
raise NotImplementedError
|
6
6
|
end
|
7
7
|
|
8
|
-
def
|
8
|
+
def create_or_skip(klass, row, finding_key)
|
9
9
|
raise NotImplementedError
|
10
10
|
end
|
11
11
|
|
12
|
-
def create_or_update(klass,
|
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,
|
5
|
-
|
6
|
-
object = klass.create!(row.to_hash)
|
4
|
+
def create(klass, row)
|
5
|
+
object = klass.create!(row.to_hash)
|
7
6
|
|
8
|
-
|
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
|
23
|
-
|
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
|
-
|
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
|
-
|
34
|
-
raise NotImplementedError
|
22
|
+
print_object_info(object)
|
35
23
|
end
|
36
24
|
end
|
37
25
|
end
|
data/lib/sowing/version.rb
CHANGED
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.
|
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-
|
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
|