oaken 0.5.0 → 0.7.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
  SHA256:
3
- metadata.gz: f7a49242a569f0460aab42af0102a0aeec30095909c6362310b45e9ce0638c43
4
- data.tar.gz: 218c22c32d91b4deaf4f3524f4a19daea964df126c3018b048a2a1119c679297
3
+ metadata.gz: 81f5e7a3ea1f5236b6e2726d1ecd24dee5698b59dc5e73f80ce618181b1dffe1
4
+ data.tar.gz: 6c4628800cdc44acd516a8e51a708fdee3d63ce506679fa5bc742b337da5c725
5
5
  SHA512:
6
- metadata.gz: 40f50f45dd4bfc45b326aacc111792b30a178446e3f51b1db9b0e9c0f95f91d6b3269a28fc3aaf8d797d5def0c6a6f1dad0ae6d61e4d1fe44aed7b994fb66dda
7
- data.tar.gz: 9c21c825239bf7bb910632abe86d6672f63dcf9fe50d0cdf42270f03aed09ff9cad3268e6e5ec70957717996193791a43c67d2cab803ad4e16efce313f29fed6
6
+ metadata.gz: 5257b630ef4ac76db5b9fceb161df47d71e1df3481d1fdcd45fb1ac35319a72568d084f6e48a158cca3217512382890d5919111f329b08798f388e46d4ff5014
7
+ data.tar.gz: 2b91b6caeede127715691ecdeab3b8355ba273d9105b11f52579f7c4da2dcfefb7bfbc243ee4cc7b7de2af6401e95e195b4c377d31502ddd0162043e243c07aa
data/README.md CHANGED
@@ -1,6 +1,17 @@
1
1
  # Oaken
2
2
 
3
- Oaken is an alternative to fixtures and/or factories to manage your development, test and some production data using data scripts.
3
+ Oaken is a new take on development and test data management for your Rails app. It blends the stability and storytelling from Fixtures with the dynamicness of FactoryBot/Fabricator.
4
+
5
+ Fixtures are stable & help you build a story of how your app and its object graph exists along with edge cases, but the UX is unfortunately a nightmare.
6
+ To trace N associations, you have to open and read N different files — there's no way to group by scenario.
7
+
8
+ FactoryBot is spray & pray. You basically say “screw it, just give me the bare minimum I need to run this test”, which slows everything down because there’s no cohesion; and the Factories are always suspect in terms of completeness. Sure, I got the test to pass by wiring these 5 Factories together but did I miss something?
9
+
10
+ Oaken instead upgrades seeds in `db/seeds.rb`, so that you can put together scenarios & also reuse the development data in tests. That way the data you see in your development browser, is the same data you work with in tests to tie it more together — especially for people who are new to your codebase.
11
+
12
+ So you get the stability of named keys, a cohesive dataset, and a story like Fixtures. But the dynamics of FactoryBot as well. And unlike FactoryBot, you’re not making tons of one-off records to handle each case.
13
+
14
+ While Fixtures and FactoryBot both load data & truncate in tests, the end result is you end up writing less data back & forth to the database because you aren’t cobbling stuff together.
4
15
 
5
16
  ## Setup
6
17
 
@@ -16,7 +27,7 @@ end
16
27
 
17
28
  This will look for deeply nested files to load in `db/seeds` and `db/seeds/#{Rails.env}` within the `accounts` and `data` directories.
18
29
 
19
- Here's what they could look like.
30
+ Here's what they could look like:
20
31
 
21
32
  ```ruby
22
33
  # db/seeds/accounts/kaspers_donuts.rb
@@ -38,12 +49,12 @@ orders.insert_all \
38
49
 
39
50
  ```ruby
40
51
  # db/seeds/data/plans.rb
41
- plans.insert :basic, title: "Basic", price_cents: 10_00
52
+ plans.upsert :basic, title: "Basic", price_cents: 10_00
42
53
  ```
43
54
 
44
55
  Seed files will generally use `create` and/or `insert`. Passing a symbol to name the record is useful when reusing the data in tests.
45
56
 
46
- Now you can run `bin/rails db:seed` plus Oaken skips executing a seed file if it knows the file hasn't been changed since the last seeding. Speedy!
57
+ Now you can run `bin/rails db:seed` and `bin/rails db:seed:replant`.
47
58
 
48
59
  ### Interlude: Directory Naming Conventions
49
60
 
@@ -53,45 +64,57 @@ Oaken has some chosen directory conventions to help strengthen your understandin
53
64
  - `db/seeds/data` for any data tables, like the plans a SaaS app has.
54
65
  - `db/seeds/tests/cases` for any specific cases that are only used in some tests, like `pagination.rb`.
55
66
 
67
+ ### Using default attributes
68
+
69
+ You can set up default attributes that's applied to created/inserted records at different levels, like this:
70
+
71
+ ```ruby
72
+ Oaken.prepare do
73
+ # Assign broad global defaults for every type.
74
+ defaults name: -> { Faker::Name.name }, public_key: -> { SecureRandom.hex }
75
+
76
+ # Assign a more specific default on one type, which overrides the global default above.
77
+ accounts.defaults name: -> { Faker::Business.name }
78
+ end
79
+ ```
80
+
81
+ > [!TIP]
82
+ > `defaults` are particularly well suited for assigning generated data with [Faker](https://github.com/faker-ruby/faker).
83
+
56
84
  ### Reusing data in tests
57
85
 
58
- With the setup above, Oaken can reuse the same data in tests like so:
86
+ With the setup above, Oaken can reuse the same data in tests like this:
59
87
 
60
88
  ```ruby
61
89
  # test/test_helper.rb
62
90
  class ActiveSupport::TestCase
63
- include Oaken.seeds
64
-
65
- # Override Minitest::Test#run to wrap each test in a transaction.
66
- def run
67
- result = nil
68
- ActiveRecord::Base.transaction(requires_new: true) do
69
- result = super
70
- raise ActiveRecord::Rollback
71
- end
72
- result
73
- end
91
+ include Oaken::TestSetup
74
92
  end
75
93
  ```
76
94
 
77
95
  Now tests have access to `accounts.kaspers_donuts` and `users.kasper` etc. that were setup in the data scripts.
78
96
 
97
+ > [!NOTE]
98
+ > For RSpec, you can put this in `spec/rails_helper.rb`:
99
+ > ```ruby
100
+ > require "oaken/rspec_setup"
101
+ > ```
102
+
79
103
  You can also load a specific seed, like this:
80
104
 
81
105
  ```ruby
82
106
  class PaginationTest < ActionDispatch::IntegrationTest
83
- seed "cases/pagination"
107
+ setup { seed "cases/pagination" }
84
108
  end
85
109
  ```
86
110
 
87
- ### Resetting cache
88
-
89
- Oaken is still early days, so you may need to reset the cache that skips seed files. Pass `OAKEN_RESET` to clear it:
90
-
91
- ```sh
92
- OAKEN_RESET=1 bin/rails db:seed
93
- OAKEN_RESET=1 bin/rails test
94
- ```
111
+ > [!NOTE]
112
+ > We're recommending having one-off seeds on an individual unit of work to help reinforce test isolation. Having some seed files be isolated also helps:
113
+ >
114
+ > - Reduce amount of junk data generated for unrelated tests
115
+ > - Make it easier to debug a particular test
116
+ > - Reduce test flakiness
117
+ > - Encourage writing seed files for specific edge-case scenarios
95
118
 
96
119
  ### Fixtures Converter
97
120
 
@@ -113,7 +136,7 @@ If bundler is not being used to manage dependencies, install the gem by executin
113
136
 
114
137
  ## Development
115
138
 
116
- After checking out the repo, run `bin/setup` to install dependencies. Then, run `cd test/dummy` and `bin/rails test` to run the tests. You can also run `bin/console` for an interactive prompt that will allow you to experiment.
139
+ After checking out the repo, run `bin/setup` to install dependencies. Then, run `bin/rails test` to run the tests. You can also run `bin/console` for an interactive prompt that will allow you to experiment.
117
140
 
118
141
  To install this gem onto your local machine, run `bundle exec rake install`. To release a new version, update the version number in `version.rb`, and then run `bundle exec rake release`, which will create a git tag for the version, push git commits and the created tag, and push the `.gem` file to [rubygems.org](https://rubygems.org).
119
142
 
@@ -86,7 +86,7 @@ class Oaken::Convert::FixturesGenerator < Rails::Generators::Base
86
86
 
87
87
  code = +"Oaken.prepare do\n"
88
88
  code << " register #{namespaces.join(", ")}\n\n" if namespaces.any?
89
- code << " load :#{@root_model.plural}, :data\n"
89
+ code << " seed :#{@root_model.plural}, :data\n"
90
90
  code << "end\n"
91
91
 
92
92
  inject_into_file "db/seeds.rb", code, before: /\A/
data/lib/oaken/railtie.rb CHANGED
@@ -1,25 +1,5 @@
1
1
  class Oaken::Railtie < Rails::Railtie
2
- initializer "oaken.defaults" do
2
+ initializer "oaken.lookup_paths" do
3
3
  Oaken.lookup_paths << "db/seeds/#{Rails.env}"
4
- Oaken.store_path = Oaken.store_path.join(Rails.env)
5
- end
6
-
7
- rake_tasks do
8
- namespace :oaken do
9
- task("reset") { Oaken.store_path.rmtree }
10
- task("reset:all") { Oaken.store_path.dirname.rmtree }
11
-
12
- task "reset:include_test" do
13
- # Some db: tasks in development also manipulate the test database.
14
- Oaken.store_path.sub("development", "test").rmtree if Rails.env.development?
15
- end
16
- end
17
-
18
- task "db:drop" => ["oaken:reset", "oaken:reset:include_test"]
19
- task "db:purge" => ["oaken:reset", "oaken:reset:include_test"]
20
- task "db:purge:all" => ["oaken:reset:all"]
21
-
22
- # db:seed:replant runs trunacte_all, after trial-and-error we need to hook into that and not the replant task.
23
- task "db:truncate_all" => "oaken:purge"
24
4
  end
25
5
  end
@@ -0,0 +1,10 @@
1
+ RSpec.configure do |config|
2
+ config.include Oaken::Seeds
3
+ config.use_transactional_fixtures = true
4
+
5
+ config.before :suite do
6
+ # Mimic fixtures by truncating before inserting.
7
+ ActiveRecord::Tasks::DatabaseTasks.truncate_all
8
+ Oaken.load_seed
9
+ end
10
+ end
data/lib/oaken/seeds.rb CHANGED
@@ -1,41 +1,110 @@
1
1
  module Oaken::Seeds
2
2
  extend self
3
3
 
4
- def self.respond_to_missing?(name, ...)
5
- Oaken.inflector.classify(name).safe_constantize || super
6
- end
4
+ # Allow assigning defaults across different types.
5
+ def self.defaults(**defaults) = attributes.merge!(**defaults)
6
+ def self.defaults_for(*keys) = attributes.slice(*keys)
7
+ def self.attributes = @attributes ||= {}.with_indifferent_access
7
8
 
9
+ # Oaken's main auto-registering logic.
10
+ #
11
+ # So when you first call e.g. `accounts.create`, we'll hit `method_missing` here
12
+ # and automatically call `register Account`.
13
+ #
14
+ # We'll also match partial and full nested namespaces like in this order:
15
+ #
16
+ # accounts => Account
17
+ # account_jobs => AccountJob | Account::Job
18
+ # account_job_tasks => AccountJobTask | Account::JobTask | Account::Job::Task
19
+ #
20
+ # If you have classes that don't follow this naming convention, you must call `register` manually.
8
21
  def self.method_missing(meth, ...)
9
- name = meth.to_s
10
- if type = Oaken.inflector.classify(name).safe_constantize
11
- register type, name
12
- public_send(name, ...)
22
+ name = meth.to_s.classify
23
+ name = name.sub!(/(?<=[a-z])(?=[A-Z])/, "::") until name.nil? or type = name.safe_constantize
24
+
25
+ if type
26
+ register type
27
+ public_send(meth, ...)
13
28
  else
14
29
  super
15
30
  end
16
31
  end
32
+ def self.respond_to_missing?(name, ...) = name.to_s.classify.safe_constantize || super
17
33
 
18
- def self.register(type, key = nil)
19
- stored = provider.new(type, key) and define_method(stored.key) { stored }
34
+ # Register a model class to be accessible as an instance method via `include Oaken::Seeds`.
35
+ # Note: Oaken's auto-register via `method_missing` means it's less likely you need to call this manually.
36
+ #
37
+ # register Account, Account::Job, Account::Job::Task
38
+ #
39
+ # Oaken uses the `table_name` of the passed classes for the method names, e.g. here they'd be
40
+ # `accounts`, `account_jobs`, and `account_job_tasks`, respectively.
41
+ def self.register(*types)
42
+ types.each do |type|
43
+ stored = provider.new(type) and define_method(stored.key) { stored }
44
+ end
20
45
  end
21
46
  def self.provider = Oaken::Stored::ActiveRecord
22
47
 
23
- singleton_class.attr_reader :loader
24
- delegate :entry, to: :loader
25
-
26
- module Loading
48
+ class << self
49
+ # Set up a general seed rule or perform a one-off seed for a test file.
50
+ #
51
+ # You can set up a general seed rule in `db/seeds.rb` like this:
52
+ #
53
+ # Oaken.prepare do
54
+ # seed :accounts # Seeds from `db/seeds/accounts/**/*.rb` and `db/seeds/<Rails.env>/accounts/**/*.rb`
55
+ # end
56
+ #
57
+ # Then if you need a test specific scenario, we recommend putting them in `db/seeds/test/cases`.
58
+ #
59
+ # Say you have `db/seeds/test/cases/pagination.rb`, you can load it like this:
60
+ #
61
+ # # test/integration/pagination_test.rb
62
+ # class PaginationTest < ActionDispatch::IntegrationTest
63
+ # setup { seed "cases/pagination" }
64
+ # end
27
65
  def seed(*directories)
28
- Oaken.lookup_paths.each do |path|
29
- directories.each do |directory|
30
- @loader = Oaken::Loader.new Pathname(path).join(directory.to_s)
31
- @loader.load_onto Oaken::Seeds
32
- end
66
+ Oaken.lookup_paths.product(directories).each do |path, directory|
67
+ load_from Pathname(path).join(directory.to_s)
33
68
  end
34
69
  end
35
- end
36
- extend Loading
37
70
 
38
- def self.included(klass)
39
- klass.extend Loading
71
+ private def load_from(path)
72
+ @loader = Oaken::Loader.new path
73
+ @loader.load_onto self
74
+ ensure
75
+ @loader = nil
76
+ end
77
+
78
+ # `section` is purely for decorative purposes to carve up `Oaken.prepare` and seed files.
79
+ #
80
+ # Oaken.prepare do
81
+ # section :roots # Just the very few top-level models like Accounts and Users.
82
+ # users.defaults email_address: -> { Faker::Internet.email }, webauthn_id: -> { SecureRandom.hex }
83
+ #
84
+ # section :stems # Models building on the roots.
85
+ #
86
+ # section :leafs # Remaining models, bulk of them, hanging off root and stem models.
87
+ #
88
+ # section do
89
+ # seed :accounts, :data
90
+ # end
91
+ # end
92
+ #
93
+ # Since `section` is defined as `def section(*, **) = yield if block_given?`, you can use
94
+ # all of Ruby's method signature flexibility to help communicate structure better.
95
+ #
96
+ # Use positional and keyword arguments, or use blocks to indent them, or combine them all.
97
+ def section(*, **)
98
+ yield if block_given?
99
+ end
40
100
  end
101
+
102
+ # Call `seed` in tests to load individual case files:
103
+ #
104
+ # class PaginationTest < ActionDispatch::IntegrationTest
105
+ # setup do
106
+ # seed "cases/pagination" # Loads `db/seeds/{,test}/cases/pagination{,**/*}.rb`
107
+ # end
108
+ # end
109
+ delegate :seed, to: Oaken::Seeds
41
110
  end
@@ -1,8 +1,9 @@
1
- class Oaken::Stored::ActiveRecord < Struct.new(:type, :key)
2
- def initialize(type, key = nil)
3
- super(type, key || Oaken.inflector.tableize(type.name))
4
- @attributes = {}
1
+ class Oaken::Stored::ActiveRecord
2
+ def initialize(type)
3
+ @type, @key = type, type.table_name
4
+ @attributes = Oaken::Seeds.defaults_for(*type.column_names)
5
5
  end
6
+ attr_reader :type, :key
6
7
  delegate :transaction, to: :type # For multi-db setups to help open a transaction on secondary connections.
7
8
  delegate :find, :insert_all, :pluck, to: :type
8
9
 
@@ -11,30 +12,34 @@ class Oaken::Stored::ActiveRecord < Struct.new(:type, :key)
11
12
  @attributes
12
13
  end
13
14
 
14
- def create(reader = nil, **attributes)
15
- lineno = caller_locations(1, 1).first.lineno
16
-
15
+ def create(label = nil, unique_by: nil, **attributes)
17
16
  attributes = @attributes.merge(attributes)
18
17
  attributes.transform_values! { _1.respond_to?(:call) ? _1.call : _1 }
19
18
 
20
- type.create!(**attributes).tap do |record|
21
- define_reader reader, record.id, lineno if reader
22
- end
23
- end
19
+ finders = attributes.slice(*unique_by)
20
+ record = type.find_by(finders)&.tap { _1.update!(**attributes) } if finders.any?
21
+ record ||= type.create!(**attributes)
24
22
 
25
- def insert(reader = nil, **attributes)
26
- lineno = caller_locations(1, 1).first.lineno
23
+ label label => record if label
24
+ record
25
+ end
27
26
 
27
+ def upsert(label = nil, unique_by: nil, **attributes)
28
28
  attributes = @attributes.merge(attributes)
29
29
  attributes.transform_values! { _1.respond_to?(:call) ? _1.call : _1 }
30
30
 
31
31
  type.new(attributes).validate!
32
- type.insert(attributes).tap do
33
- define_reader reader, type.where(attributes).pick(:id), lineno if reader
34
- end
32
+ record = type.new(id: type.upsert(attributes, unique_by: unique_by, returning: :id).rows.first.first)
33
+ label label => record if label
34
+ record
35
35
  end
36
36
 
37
- def define_reader(...)
38
- Oaken::Seeds.entry.define_reader(self, ...)
37
+ def label(**labels)
38
+ # TODO: Fix hardcoding of db/seeds instead of using Oaken.lookup_paths
39
+ location = caller_locations(1, 6).find { _1.path.match? /db\/seeds\// }
40
+
41
+ labels.each do |label, record|
42
+ class_eval "def #{label} = find(#{record.id})", location.path, location.lineno
43
+ end
39
44
  end
40
45
  end
@@ -0,0 +1,24 @@
1
+ module Oaken::TestSetup
2
+ include Oaken::Seeds
3
+
4
+ def self.included(klass)
5
+ klass.fixtures # Rely on fixtures to setup a shared connection pool and wrap tests in transactions.
6
+ klass.parallelize_setup { Oaken.load_seed } # No need to truncate as parallel test databases are always empty.
7
+ klass.prepend BeforeSetup
8
+ end
9
+
10
+ module BeforeSetup
11
+ # We must inject late enough to call `should_parallelize?`, but before fixtures' `before_setup`.
12
+ #
13
+ # So we prepend into `before_setup` and later `super` to have fixtures wrap tests in transactions.
14
+ def before_setup
15
+ unless Minitest.parallel_executor.send(:should_parallelize?)
16
+ ActiveRecord::Tasks::DatabaseTasks.truncate_all # Mimic fixtures by truncating before inserting.
17
+ Oaken.load_seed
18
+ end
19
+
20
+ Oaken::TestSetup::BeforeSetup.remove_method :before_setup # Only run once, so remove before passing to fixtures in `super`.
21
+ super
22
+ end
23
+ end
24
+ end
data/lib/oaken/version.rb CHANGED
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Oaken
4
- VERSION = "0.5.0"
4
+ VERSION = "0.7.0"
5
5
  end
data/lib/oaken.rb CHANGED
@@ -6,64 +6,30 @@ require "pathname"
6
6
  module Oaken
7
7
  class Error < StandardError; end
8
8
 
9
- autoload :Seeds, "oaken/seeds"
10
- autoload :Entry, "oaken/entry"
9
+ autoload :Seeds, "oaken/seeds"
10
+ autoload :TestSetup, "oaken/test_setup"
11
11
 
12
12
  module Stored
13
13
  autoload :ActiveRecord, "oaken/stored/active_record"
14
14
  end
15
15
 
16
- class Inflector
17
- def tableize(string)
18
- string.gsub(/(?<=[a-z])(?=[A-Z])/, "_").gsub("::", "_").tap(&:downcase!) << "s"
19
- end
20
-
21
- def classify(string)
22
- string.chomp("s").gsub(/_([a-z])/) { $1.upcase }.sub(/^\w/, &:upcase)
23
- end
24
- end
25
-
26
- singleton_class.attr_accessor :inflector
27
- @inflector = Inflector.new
28
-
29
16
  singleton_class.attr_reader :lookup_paths
30
17
  @lookup_paths = ["db/seeds"]
31
18
 
32
- singleton_class.attr_accessor :store_path
33
- @store_path = Pathname.new "tmp/oaken/store"
34
-
35
19
  class Loader
36
- attr_reader :entry
37
-
38
20
  def initialize(path)
39
- @entries, @entry = Entry.within(path), nil
21
+ @entries = Pathname.glob("#{path}{,/**/*}.rb").sort
40
22
  end
41
23
 
42
- def load_onto(seeds)
43
- @entries.each do |entry|
44
- @entry = entry
45
- @entry.load_onto seeds
24
+ def load_onto(seeds) = @entries.each do |path|
25
+ ActiveRecord::Base.transaction do
26
+ seeds.class_eval path.read, path.to_s
46
27
  end
47
28
  end
48
29
  end
49
30
 
50
- def self.transaction(&block)
51
- ActiveRecord::Base.transaction(&block)
52
- end
53
-
54
- def self.prepare(&block)
55
- store_path.rmtree if ENV["OAKEN_RESET"]
56
- Seeds.instance_eval(&block)
57
- Seeds
58
- end
59
-
60
- def self.seeds
61
- unless defined?(@loaded)
62
- @loaded = true
63
- Rails.application.load_seed
64
- end
65
- Seeds
66
- end
31
+ def self.prepare(&block) = Seeds.instance_eval(&block)
32
+ def self.load_seed = Rails.application.load_seed
67
33
  end
68
34
 
69
35
  require_relative "oaken/railtie" if defined?(Rails::Railtie)
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: oaken
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.5.0
4
+ version: 0.7.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Kasper Timm Hansen
8
8
  autorequire:
9
9
  bindir: exe
10
10
  cert_chain: []
11
- date: 2023-11-16 00:00:00.000000000 Z
11
+ date: 2024-06-30 00:00:00.000000000 Z
12
12
  dependencies: []
13
13
  description:
14
14
  email:
@@ -24,10 +24,11 @@ files:
24
24
  - Rakefile
25
25
  - lib/generators/oaken/convert/fixtures_generator.rb
26
26
  - lib/oaken.rb
27
- - lib/oaken/entry.rb
28
27
  - lib/oaken/railtie.rb
28
+ - lib/oaken/rspec_setup.rb
29
29
  - lib/oaken/seeds.rb
30
30
  - lib/oaken/stored/active_record.rb
31
+ - lib/oaken/test_setup.rb
31
32
  - lib/oaken/version.rb
32
33
  homepage: https://github.com/kaspth/oaken
33
34
  licenses:
@@ -52,7 +53,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
52
53
  - !ruby/object:Gem::Version
53
54
  version: '0'
54
55
  requirements: []
55
- rubygems_version: 3.4.19
56
+ rubygems_version: 3.5.10
56
57
  signing_key:
57
58
  specification_version: 4
58
59
  summary: Oaken aims to blend your Fixtures/Factories and levels up your database seeds.
data/lib/oaken/entry.rb DELETED
@@ -1,60 +0,0 @@
1
- require "digest/md5"
2
- require "pstore"
3
-
4
- class Oaken::Entry < DelegateClass(PStore)
5
- def self.store_accessor(name)
6
- define_method(name) { self[name] } and define_method("#{name}=") { |value| self[name] = value }
7
- end
8
- store_accessor :checksum
9
- store_accessor :readers
10
-
11
- def self.within(directory)
12
- Pathname.glob("#{directory}{,/**/*}.{rb,sql}").sort.map { new _1 }
13
- end
14
-
15
- def initialize(pathname)
16
- @file, @pathname = pathname.to_s, pathname
17
- @computed_checksum = Digest::MD5.hexdigest(@pathname.read)
18
-
19
- prepared_store_path = Oaken.store_path.join(pathname).tap { _1.dirname.mkpath }
20
- super PStore.new(prepared_store_path)
21
- end
22
-
23
- def load_onto(seeds)
24
- transaction do
25
- if replay?
26
- puts "Replaying #{@file}…"
27
- readers.each do |key, *args|
28
- define_reader(seeds.send(key), *args)
29
- end
30
- else
31
- reset
32
-
33
- case @pathname.extname
34
- in ".rb" then seeds.class_eval @pathname.read, @file
35
- in ".sql" then ActiveRecord::Base.connection.execute @pathname.read
36
- end
37
- end
38
- end
39
- end
40
-
41
- def transaction(&block)
42
- super do
43
- Oaken.transaction(&block)
44
- end
45
- end
46
-
47
- def replay?
48
- checksum == @computed_checksum
49
- end
50
-
51
- def reset
52
- self.checksum = @computed_checksum
53
- self.readers = Set.new
54
- end
55
-
56
- def define_reader(stored, name, id, lineno)
57
- stored.instance_eval "def #{name}; find #{id}; end", @file, lineno
58
- readers << [stored.key, name, id, lineno]
59
- end
60
- end