oaken 0.7.1 → 0.8.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/README.md +29 -2
- data/lib/oaken/railtie.rb +5 -3
- data/lib/oaken/rspec_setup.rb +1 -3
- data/lib/oaken/seeds.rb +16 -24
- data/lib/oaken/stored/active_record.rb +54 -16
- data/lib/oaken/test_setup.rb +2 -4
- data/lib/oaken/type.rb +26 -0
- data/lib/oaken/version.rb +1 -1
- data/lib/oaken.rb +30 -3
- metadata +4 -7
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 43bfd3d777a6a7060eacfda4d23ee10bdb19932547bd6c52e82757b77e897d0e
|
4
|
+
data.tar.gz: 2c81615943933ca2a22f375f2d7afedd07078ea1ca4c71f609970273da5fe0a5
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 2e13841a16e33779150ba1ba45cf7c4407f1d2f7ead9fb29ca886b93afe2a78fc3394c39899b1f550878b2e0f915f6ceeb52e80582c0fb167e3784a68f453002
|
7
|
+
data.tar.gz: 751a8d8613e43d1af6c5f76ab73be8ee6acc1aa1a6d2b0a3c58250540555d5da1503860e7f3d582d4107da097b840add7e186fe6067344c3bfabd8bc36b5a836
|
data/README.md
CHANGED
@@ -3,7 +3,7 @@
|
|
3
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
4
|
|
5
5
|
> But seriously; Oaken is one of the single greatest tools I've added to my belt in the past year
|
6
|
-
>
|
6
|
+
>
|
7
7
|
> It's made cross-environment shared data, data prepping for demos, edge-case tests, and overall development much more reliable & shareable across a team
|
8
8
|
> [@tcannonfodder](https://github.com/tcannonfodder)
|
9
9
|
|
@@ -67,7 +67,7 @@ Oaken has some chosen directory conventions to help strengthen your understandin
|
|
67
67
|
|
68
68
|
- Have a directory for your top-level model, like `Account`, `Team`, `Organization`, that's why we have `db/seeds/accounts` above.
|
69
69
|
- `db/seeds/data` for any data tables, like the plans a SaaS app has.
|
70
|
-
- `db/seeds/
|
70
|
+
- `db/seeds/test/cases` for any specific cases that are only used in some tests, like `pagination.rb`.
|
71
71
|
|
72
72
|
### Using default attributes
|
73
73
|
|
@@ -113,6 +113,14 @@ class PaginationTest < ActionDispatch::IntegrationTest
|
|
113
113
|
end
|
114
114
|
```
|
115
115
|
|
116
|
+
And in RSpec:
|
117
|
+
|
118
|
+
```ruby
|
119
|
+
RSpec.describe "Pagination", type: :feature do
|
120
|
+
before { seed "cases/pagination" }
|
121
|
+
end
|
122
|
+
```
|
123
|
+
|
116
124
|
> [!NOTE]
|
117
125
|
> 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:
|
118
126
|
>
|
@@ -129,6 +137,25 @@ You can convert your Rails fixtures to Oaken's seeds by running:
|
|
129
137
|
|
130
138
|
This will convert anything in test/fixtures to db/seeds. E.g. `test/fixtures/users.yml` becomes `db/seeds/users.rb`.
|
131
139
|
|
140
|
+
### Disable fixtures
|
141
|
+
|
142
|
+
IF you've fully converted to Oaken you may no longer want fixtures when running Rails' generators,
|
143
|
+
so you can disable generating them in `config/application.rb` like this:
|
144
|
+
|
145
|
+
```ruby
|
146
|
+
module YourApp
|
147
|
+
class Application < Rails::Application
|
148
|
+
# We prefer Oaken to fixtures, so we disable them here.
|
149
|
+
config.app_generators { _1.test_framework _1.test_framework, fixture: false }
|
150
|
+
end
|
151
|
+
end
|
152
|
+
```
|
153
|
+
|
154
|
+
The `test_framework` repeating is to preserve `:test_unit` or `:rspec` respectively.
|
155
|
+
|
156
|
+
> [!NOTE]
|
157
|
+
> If you're using `FactoryBot` as well, you don't need to do this since it already replaces fixtures for you.
|
158
|
+
|
132
159
|
## Installation
|
133
160
|
|
134
161
|
Install the gem and add to the application's Gemfile by executing:
|
data/lib/oaken/railtie.rb
CHANGED
@@ -1,5 +1,7 @@
|
|
1
|
-
|
2
|
-
|
3
|
-
|
1
|
+
module Oaken
|
2
|
+
class Railtie < Rails::Railtie
|
3
|
+
initializer "oaken.lookup_paths" do
|
4
|
+
Oaken.lookup_paths << "db/seeds/#{Rails.env}"
|
5
|
+
end
|
4
6
|
end
|
5
7
|
end
|
data/lib/oaken/rspec_setup.rb
CHANGED
data/lib/oaken/seeds.rb
CHANGED
@@ -11,36 +11,37 @@ module Oaken::Seeds
|
|
11
11
|
# So when you first call e.g. `accounts.create`, we'll hit `method_missing` here
|
12
12
|
# and automatically call `register Account`.
|
13
13
|
#
|
14
|
-
# We'll also match partial and full nested namespaces
|
14
|
+
# We'll also match partial and full nested namespaces:
|
15
15
|
#
|
16
16
|
# accounts => Account
|
17
|
-
# account_jobs =>
|
18
|
-
# account_job_tasks =>
|
17
|
+
# account_jobs => Account::Job | AccountJob
|
18
|
+
# account_job_tasks => Account::JobTask | Account::Job::Task | AccountJob::Task | AccountJobTask
|
19
19
|
#
|
20
|
-
# If you have classes that don't follow
|
20
|
+
# If you have classes that don't follow these naming conventions, you must call `register` manually.
|
21
21
|
def self.method_missing(meth, ...)
|
22
|
-
|
23
|
-
|
24
|
-
|
25
|
-
if type
|
26
|
-
register type
|
22
|
+
if type = Oaken::Type.for(meth.to_s).locate
|
23
|
+
register type, as: meth
|
27
24
|
public_send(meth, ...)
|
28
25
|
else
|
29
26
|
super
|
30
27
|
end
|
31
28
|
end
|
32
|
-
def self.respond_to_missing?(
|
29
|
+
def self.respond_to_missing?(meth, ...) = Oaken::Type.for(meth.to_s).locate || super
|
33
30
|
|
34
31
|
# Register a model class to be accessible as an instance method via `include Oaken::Seeds`.
|
35
32
|
# Note: Oaken's auto-register via `method_missing` means it's less likely you need to call this manually.
|
36
33
|
#
|
37
34
|
# register Account, Account::Job, Account::Job::Task
|
38
35
|
#
|
39
|
-
# Oaken uses
|
36
|
+
# Oaken uses `name.tableize.tr("/", "_")` on the passed classes for the method names, so they're
|
40
37
|
# `accounts`, `account_jobs`, and `account_job_tasks`, respectively.
|
41
|
-
|
38
|
+
#
|
39
|
+
# You can also pass an explicit `as:` option, if you'd like:
|
40
|
+
#
|
41
|
+
# register User, as: :something_else
|
42
|
+
def self.register(*types, as: nil)
|
42
43
|
types.each do |type|
|
43
|
-
stored = provider.new(type) and define_method(
|
44
|
+
stored = provider.new(type) and define_method(as || type.name.tableize.tr("/", "_")) { stored }
|
44
45
|
end
|
45
46
|
end
|
46
47
|
def self.provider = Oaken::Stored::ActiveRecord
|
@@ -62,17 +63,8 @@ module Oaken::Seeds
|
|
62
63
|
# class PaginationTest < ActionDispatch::IntegrationTest
|
63
64
|
# setup { seed "cases/pagination" }
|
64
65
|
# end
|
65
|
-
def seed(*
|
66
|
-
Oaken.
|
67
|
-
load_from Pathname(path).join(directory.to_s)
|
68
|
-
end
|
69
|
-
end
|
70
|
-
|
71
|
-
private def load_from(path)
|
72
|
-
@loader = Oaken::Loader.new path
|
73
|
-
@loader.load_onto self
|
74
|
-
ensure
|
75
|
-
@loader = nil
|
66
|
+
def seed(*identifiers)
|
67
|
+
Oaken::Loader.from(identifiers).load_onto self
|
76
68
|
end
|
77
69
|
|
78
70
|
# `section` is purely for decorative purposes to carve up `Oaken.prepare` and seed files.
|
@@ -1,20 +1,14 @@
|
|
1
1
|
class Oaken::Stored::ActiveRecord
|
2
2
|
def initialize(type)
|
3
|
-
@type
|
3
|
+
@type = type
|
4
4
|
@attributes = Oaken::Seeds.defaults_for(*type.column_names)
|
5
5
|
end
|
6
|
-
attr_reader :type
|
6
|
+
attr_reader :type
|
7
7
|
delegate :transaction, to: :type # For multi-db setups to help open a transaction on secondary connections.
|
8
8
|
delegate :find, :insert_all, :pluck, to: :type
|
9
9
|
|
10
|
-
def defaults(**attributes)
|
11
|
-
@attributes = @attributes.merge(attributes)
|
12
|
-
@attributes
|
13
|
-
end
|
14
|
-
|
15
10
|
def create(label = nil, unique_by: nil, **attributes)
|
16
|
-
attributes =
|
17
|
-
attributes.transform_values! { _1.respond_to?(:call) ? _1.call : _1 }
|
11
|
+
attributes = attributes_for(**attributes)
|
18
12
|
|
19
13
|
finders = attributes.slice(*unique_by)
|
20
14
|
record = type.find_by(finders)&.tap { _1.update!(**attributes) } if finders.any?
|
@@ -25,8 +19,7 @@ class Oaken::Stored::ActiveRecord
|
|
25
19
|
end
|
26
20
|
|
27
21
|
def upsert(label = nil, unique_by: nil, **attributes)
|
28
|
-
attributes =
|
29
|
-
attributes.transform_values! { _1.respond_to?(:call) ? _1.call : _1 }
|
22
|
+
attributes = attributes_for(**attributes)
|
30
23
|
|
31
24
|
type.new(attributes).validate!
|
32
25
|
record = type.new(id: type.upsert(attributes, unique_by: unique_by, returning: :id).rows.first.first)
|
@@ -34,12 +27,57 @@ class Oaken::Stored::ActiveRecord
|
|
34
27
|
record
|
35
28
|
end
|
36
29
|
|
30
|
+
# Build attributes used for `create`/`upsert`, applying any global and per-type `defaults`.
|
31
|
+
#
|
32
|
+
# # db/seeds.rb
|
33
|
+
# Oaken.prepare do
|
34
|
+
# defaults name: -> { "Global" }, email_address: -> { … }
|
35
|
+
# users.defaults name: -> { Faker::Name.name } # This `name` takes precedence on users.
|
36
|
+
# end
|
37
|
+
#
|
38
|
+
# users.attributes_for(email_address: "user@example.com") # => { name: "Some Faker Name", email_address: "user@example.com" }
|
39
|
+
def attributes_for(**attributes)
|
40
|
+
@attributes.merge(attributes).transform_values! { _1.respond_to?(:call) ? _1.call : _1 }
|
41
|
+
end
|
42
|
+
|
43
|
+
# Set defaults for this type:
|
44
|
+
#
|
45
|
+
# # db/seeds.rb
|
46
|
+
# Oaken.prepare do
|
47
|
+
# defaults name: -> { "Global" }, email_address: -> { … }
|
48
|
+
# users.defaults name: -> { Faker::Name.name } # This `name` takes precedence on users.
|
49
|
+
# end
|
50
|
+
#
|
51
|
+
# These defaults are used and evaluated in `create`/`upsert`/`attributes_for`.
|
52
|
+
#
|
53
|
+
# users.create # => Uses the users' default `name` and the global `email_address`
|
54
|
+
def defaults(**attributes)
|
55
|
+
@attributes = @attributes.merge(attributes)
|
56
|
+
@attributes
|
57
|
+
end
|
58
|
+
|
59
|
+
# Expose a record instance that's setup outside of using `create`/`upsert`. Like this:
|
60
|
+
#
|
61
|
+
# users.label someone: User.create!(name: "Someone")
|
62
|
+
# users.label someone: FactoryBot.create(:user, name: "Someone")
|
63
|
+
#
|
64
|
+
# Now `users.someone` returns the record instance.
|
65
|
+
#
|
66
|
+
# Ruby's Hash argument forwarding also works:
|
67
|
+
#
|
68
|
+
# someone = users.create(name: "Someone")
|
69
|
+
# someone_else = users.create(name: "Someone Else")
|
70
|
+
# users.label someone:, someone_else:
|
71
|
+
#
|
72
|
+
# Note: `users.method(:someone).source_location` also points back to the file and line of the `label` call.
|
37
73
|
def label(**labels)
|
38
|
-
|
39
|
-
|
74
|
+
labels.each { |label, record| _label label, record.id }
|
75
|
+
end
|
76
|
+
|
77
|
+
private def _label(name, id)
|
78
|
+
raise ArgumentError, "you can only define labelled records outside of tests" \
|
79
|
+
unless location = Oaken::Loader.definition_location
|
40
80
|
|
41
|
-
|
42
|
-
class_eval "def #{label} = find(#{record.id})", location.path, location.lineno
|
43
|
-
end
|
81
|
+
class_eval "def #{name} = find(#{id.inspect})", location.path, location.lineno
|
44
82
|
end
|
45
83
|
end
|
data/lib/oaken/test_setup.rb
CHANGED
@@ -12,10 +12,8 @@ module Oaken::TestSetup
|
|
12
12
|
#
|
13
13
|
# So we prepend into `before_setup` and later `super` to have fixtures wrap tests in transactions.
|
14
14
|
def before_setup
|
15
|
-
|
16
|
-
|
17
|
-
Oaken.load_seed
|
18
|
-
end
|
15
|
+
# `should_parallelize?` is only defined when Rails' test `parallelize` macro has been called.
|
16
|
+
Oaken.replant_seed unless Minitest.parallel_executor.then { _1.respond_to?(:should_parallelize?, true) && _1.send(:should_parallelize?) }
|
19
17
|
|
20
18
|
Oaken::TestSetup::BeforeSetup.remove_method :before_setup # Only run once, so remove before passing to fixtures in `super`.
|
21
19
|
super
|
data/lib/oaken/type.rb
ADDED
@@ -0,0 +1,26 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
class Oaken::Type < Struct.new(:name, :gsub)
|
4
|
+
def self.for(name) = new(name, name.classify.gsub(/(?<=[a-z])(?=[A-Z])/))
|
5
|
+
|
6
|
+
def locate
|
7
|
+
possible_consts.filter_map(&:safe_constantize).first
|
8
|
+
end
|
9
|
+
|
10
|
+
def possible_consts
|
11
|
+
separator_matrixes.fetch(gsub.count).map { |seps| gsub.with_index { seps[_2] } }
|
12
|
+
rescue KeyError
|
13
|
+
raise ArgumentError, "can't resolve #{name} to an object, please call register manually"
|
14
|
+
end
|
15
|
+
|
16
|
+
private
|
17
|
+
# TODO: Remove after dropping Ruby 3.1 support
|
18
|
+
if Enumerator.respond_to?(:product)
|
19
|
+
def self.product(...) = Enumerator.product(...)
|
20
|
+
else
|
21
|
+
def self.product(first = nil, *rest) = first&.product(*rest) || [[]]
|
22
|
+
end
|
23
|
+
|
24
|
+
separator_matrixes = (0..3).to_h { |size| [size, product(*[["::", ""]].*(size)).lazy] }
|
25
|
+
define_method(:separator_matrixes) { separator_matrixes }
|
26
|
+
end
|
data/lib/oaken/version.rb
CHANGED
data/lib/oaken.rb
CHANGED
@@ -7,6 +7,7 @@ module Oaken
|
|
7
7
|
class Error < StandardError; end
|
8
8
|
|
9
9
|
autoload :Seeds, "oaken/seeds"
|
10
|
+
autoload :Type, "oaken/type"
|
10
11
|
autoload :TestSetup, "oaken/test_setup"
|
11
12
|
|
12
13
|
module Stored
|
@@ -16,9 +17,22 @@ module Oaken
|
|
16
17
|
singleton_class.attr_reader :lookup_paths
|
17
18
|
@lookup_paths = ["db/seeds"]
|
18
19
|
|
20
|
+
def self.glob(identifier)
|
21
|
+
patterns = lookup_paths.map { File.join _1, "#{identifier}{,/**/*}.rb" }
|
22
|
+
|
23
|
+
Pathname.glob(patterns).tap do |found|
|
24
|
+
raise NoSeedsFoundError, "found no seed files for #{identifier.inspect}" if found.none?
|
25
|
+
end
|
26
|
+
end
|
27
|
+
NoSeedsFoundError = Class.new ArgumentError
|
28
|
+
|
19
29
|
class Loader
|
20
|
-
def
|
21
|
-
|
30
|
+
def self.from(identifiers)
|
31
|
+
new identifiers.flat_map { Oaken.glob _1 }
|
32
|
+
end
|
33
|
+
|
34
|
+
def initialize(entries)
|
35
|
+
@entries = entries
|
22
36
|
end
|
23
37
|
|
24
38
|
def load_onto(seeds) = @entries.each do |path|
|
@@ -26,9 +40,22 @@ module Oaken
|
|
26
40
|
seeds.class_eval path.read, path.to_s
|
27
41
|
end
|
28
42
|
end
|
43
|
+
|
44
|
+
def self.definition_location
|
45
|
+
# Trickery abounds! Due to Ruby's `caller_locations` + our `load_onto`'s `class_eval` above
|
46
|
+
# we can use this format to detect the location in the seed file where the call came from.
|
47
|
+
caller_locations(2, 8).find { _1.label.match? /block .*?load_onto/ }
|
48
|
+
end
|
29
49
|
end
|
30
50
|
|
31
|
-
def self.prepare(&block)
|
51
|
+
def self.prepare(&block)
|
52
|
+
Seeds.instance_eval(&block)
|
53
|
+
end
|
54
|
+
|
55
|
+
def self.replant_seed
|
56
|
+
ActiveRecord::Tasks::DatabaseTasks.truncate_all
|
57
|
+
load_seed
|
58
|
+
end
|
32
59
|
def self.load_seed = Rails.application.load_seed
|
33
60
|
end
|
34
61
|
|
metadata
CHANGED
@@ -1,16 +1,14 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: oaken
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 0.
|
4
|
+
version: 0.8.0
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Kasper Timm Hansen
|
8
|
-
autorequire:
|
9
8
|
bindir: exe
|
10
9
|
cert_chain: []
|
11
|
-
date:
|
10
|
+
date: 1980-01-02 00:00:00.000000000 Z
|
12
11
|
dependencies: []
|
13
|
-
description:
|
14
12
|
email:
|
15
13
|
- hey@kaspth.com
|
16
14
|
executables: []
|
@@ -29,6 +27,7 @@ files:
|
|
29
27
|
- lib/oaken/seeds.rb
|
30
28
|
- lib/oaken/stored/active_record.rb
|
31
29
|
- lib/oaken/test_setup.rb
|
30
|
+
- lib/oaken/type.rb
|
32
31
|
- lib/oaken/version.rb
|
33
32
|
homepage: https://github.com/kaspth/oaken
|
34
33
|
licenses:
|
@@ -38,7 +37,6 @@ metadata:
|
|
38
37
|
homepage_uri: https://github.com/kaspth/oaken
|
39
38
|
source_code_uri: https://github.com/kaspth/oaken
|
40
39
|
changelog_uri: https://github.com/kaspth/oaken/blob/main/CHANGELOG.md
|
41
|
-
post_install_message:
|
42
40
|
rdoc_options: []
|
43
41
|
require_paths:
|
44
42
|
- lib
|
@@ -53,8 +51,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
|
|
53
51
|
- !ruby/object:Gem::Version
|
54
52
|
version: '0'
|
55
53
|
requirements: []
|
56
|
-
rubygems_version: 3.
|
57
|
-
signing_key:
|
54
|
+
rubygems_version: 3.6.8
|
58
55
|
specification_version: 4
|
59
56
|
summary: Oaken aims to blend your Fixtures/Factories and levels up your database seeds.
|
60
57
|
test_files: []
|