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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 5de0c10ab76ddbee3960eda8a9cbf2c8a5b9e1065ded625761c3333a5e3682da
4
- data.tar.gz: 42f27cb3adcc4c4d73d8267dcf7f1e30e0923b2773626001e4689fa166cd132d
3
+ metadata.gz: 43bfd3d777a6a7060eacfda4d23ee10bdb19932547bd6c52e82757b77e897d0e
4
+ data.tar.gz: 2c81615943933ca2a22f375f2d7afedd07078ea1ca4c71f609970273da5fe0a5
5
5
  SHA512:
6
- metadata.gz: f933b1ae38aa3b27cd9682282e64fa7e13f1818fa9133c90023d2d0addc5ed3f8f38fc9cac51e55b6b572db7b928fd1890ad4f25881f0c5c7391888dc42e12cc
7
- data.tar.gz: 5cddcfc3d65ed8299bb15be1c716e0165bac2f35c4a885f7a80ae0eab1b83085b3883f8edd62df55cba182957a667186b5f6778b3a47d2debc15e4a465666956
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/tests/cases` for any specific cases that are only used in some tests, like `pagination.rb`.
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
- class Oaken::Railtie < Rails::Railtie
2
- initializer "oaken.lookup_paths" do
3
- Oaken.lookup_paths << "db/seeds/#{Rails.env}"
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
@@ -3,8 +3,6 @@ RSpec.configure do |config|
3
3
  config.use_transactional_fixtures = true
4
4
 
5
5
  config.before :suite do
6
- # Mimic fixtures by truncating before inserting.
7
- ActiveRecord::Tasks::DatabaseTasks.truncate_all
8
- Oaken.load_seed
6
+ Oaken.replant_seed
9
7
  end
10
8
  end
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 like in this order:
14
+ # We'll also match partial and full nested namespaces:
15
15
  #
16
16
  # accounts => Account
17
- # account_jobs => AccountJob | Account::Job
18
- # account_job_tasks => AccountJobTask | Account::JobTask | Account::Job::Task
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 this naming convention, you must call `register` manually.
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
- 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
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?(name, ...) = name.to_s.classify.safe_constantize || super
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 the `table_name` of the passed classes for the method names, e.g. here they'd be
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
- def self.register(*types)
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(stored.key) { stored }
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(*directories)
66
- Oaken.lookup_paths.product(directories).each do |path, directory|
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, @key = type, type.table_name
3
+ @type = type
4
4
  @attributes = Oaken::Seeds.defaults_for(*type.column_names)
5
5
  end
6
- attr_reader :type, :key
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 = @attributes.merge(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 = @attributes.merge(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
- # TODO: Fix hardcoding of db/seeds instead of using Oaken.lookup_paths
39
- location = caller_locations(1, 6).find { _1.path.match? /db\/seeds\// }
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
- labels.each do |label, record|
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
@@ -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
- 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
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
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Oaken
4
- VERSION = "0.7.1"
4
+ VERSION = "0.8.0"
5
5
  end
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 initialize(path)
21
- @entries = Pathname.glob("#{path}{,/**/*}.rb").sort
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) = Seeds.instance_eval(&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.7.1
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: 2024-12-10 00:00:00.000000000 Z
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.5.18
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: []