with_model 2.2.0 → 2.3.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/.github/dependabot.yml +22 -6
- data/.github/workflows/ci.yml +64 -29
- data/CHANGELOG.md +19 -0
- data/CONTRIBUTING.md +83 -0
- data/Gemfile +2 -7
- data/LICENSE +1 -1
- data/README.md +116 -22
- data/lib/with_model/invalid_superclass.rb +15 -0
- data/lib/with_model/missing_superclass.rb +14 -0
- data/lib/with_model/model/dsl.rb +5 -2
- data/lib/with_model/model.rb +86 -6
- data/lib/with_model/null_table.rb +98 -0
- data/lib/with_model/table.rb +12 -0
- data/lib/with_model/version.rb +1 -1
- data/lib/with_model.rb +39 -1
- data/spec/active_record_behaviors_spec.rb +4 -1
- data/spec/descendants_tracking_spec.rb +1 -0
- data/spec/readme_spec.rb +61 -12
- data/spec/spec_helper.rb +2 -0
- data/spec/table_false_spec.rb +516 -0
- data/spec/with_model_spec.rb +31 -8
- data/test/declaration_order_test.rb +61 -0
- data/test/spec_dsl_test.rb +48 -0
- data/test/sti_test.rb +68 -0
- data/test/test_helper.rb +2 -0
- data/with_model.gemspec +2 -2
- metadata +14 -6
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 0bac7bd2d2e529887f1e9bd82117cc809f23b336a8acaa2b90e4fb9305c96fe2
|
|
4
|
+
data.tar.gz: 1867b36f42957d744858fcefe70f53c58b0c5f39b951a4749a969c39c46b42f0
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: eec34fe045c932273600775ccf7b2719dad1d10d6631baf43b9b7d54eec50bc4592d24cc1da1e9af7a844e7fbc73f707a0027d9f58c891afd18fb934e1f796d9
|
|
7
|
+
data.tar.gz: 5720ad24f60c91dee5b1eac354a0ca3d3f00d6c65e5d2f45fc76a310d6330564f43ec14924f168edc064121e9cc70b4ca7483c08a33587ea90f20031fff3fba2
|
data/.github/dependabot.yml
CHANGED
|
@@ -1,8 +1,24 @@
|
|
|
1
1
|
version: 2
|
|
2
2
|
updates:
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
3
|
+
# Gemfile.lock is not checked in and the Gemfile deliberately leaves versions
|
|
4
|
+
# to Bundler, so there is nothing here for Dependabot to bump routinely. It is
|
|
5
|
+
# listed to receive security advisories for the constraints that do exist.
|
|
6
|
+
- package-ecosystem: bundler
|
|
7
|
+
directory: "/"
|
|
8
|
+
schedule:
|
|
9
|
+
interval: weekly
|
|
10
|
+
day: saturday
|
|
11
|
+
open-pull-requests-limit: 10
|
|
12
|
+
|
|
13
|
+
# The workflow pins actions by major version, which does not float across
|
|
14
|
+
# majors: actions/checkout sat four majors behind until someone noticed by
|
|
15
|
+
# hand. One grouped pull request keeps them current without a PR per action.
|
|
16
|
+
- package-ecosystem: github-actions
|
|
17
|
+
directory: "/"
|
|
18
|
+
schedule:
|
|
19
|
+
interval: weekly
|
|
20
|
+
day: saturday
|
|
21
|
+
groups:
|
|
22
|
+
actions:
|
|
23
|
+
patterns:
|
|
24
|
+
- "*"
|
data/.github/workflows/ci.yml
CHANGED
|
@@ -6,50 +6,85 @@ on:
|
|
|
6
6
|
- master
|
|
7
7
|
- github-actions
|
|
8
8
|
pull_request:
|
|
9
|
+
schedule:
|
|
10
|
+
- cron: "0 18 * * 6" # Saturdays at 12pm CST
|
|
11
|
+
workflow_dispatch:
|
|
12
|
+
|
|
13
|
+
permissions:
|
|
14
|
+
contents: read
|
|
15
|
+
|
|
16
|
+
concurrency:
|
|
17
|
+
group: ${{ github.workflow }}-${{ github.ref }}
|
|
18
|
+
cancel-in-progress: ${{ github.event_name == 'pull_request' }}
|
|
9
19
|
|
|
10
20
|
jobs:
|
|
11
21
|
test:
|
|
22
|
+
name: Ruby ${{ matrix.ruby-version }} / ${{ matrix.active-record.label }}
|
|
12
23
|
runs-on: ubuntu-latest
|
|
24
|
+
timeout-minutes: 20
|
|
13
25
|
env:
|
|
14
26
|
CI: true
|
|
15
27
|
strategy:
|
|
16
28
|
fail-fast: false
|
|
17
29
|
matrix:
|
|
18
|
-
ruby-version: [
|
|
19
|
-
active-record
|
|
20
|
-
-
|
|
21
|
-
|
|
22
|
-
-
|
|
23
|
-
|
|
30
|
+
ruby-version: ["3.3", "3.4", "4.0"]
|
|
31
|
+
active-record:
|
|
32
|
+
- label: AR 7.2
|
|
33
|
+
env: ACTIVE_RECORD_VERSION=~> 7.2.0
|
|
34
|
+
- label: AR 8.0
|
|
35
|
+
env: ACTIVE_RECORD_VERSION=~> 8.0.0
|
|
36
|
+
- label: AR 8.1
|
|
37
|
+
env: ACTIVE_RECORD_VERSION=~> 8.1.0
|
|
24
38
|
allow-failure: [false]
|
|
25
|
-
exclude:
|
|
26
|
-
- ruby-version: '3.1'
|
|
27
|
-
active-record-version-env: ACTIVE_RECORD_VERSION="~> 8.0.0"
|
|
28
39
|
include:
|
|
29
|
-
- ruby-version:
|
|
30
|
-
active-record
|
|
40
|
+
- ruby-version: "4.0"
|
|
41
|
+
active-record:
|
|
42
|
+
label: AR main
|
|
43
|
+
env: ACTIVE_RECORD_BRANCH=main
|
|
31
44
|
allow-failure: true
|
|
32
|
-
- ruby-version:
|
|
33
|
-
active-record
|
|
45
|
+
- ruby-version: "4.0"
|
|
46
|
+
active-record:
|
|
47
|
+
label: AR 8-1-stable
|
|
48
|
+
env: ACTIVE_RECORD_BRANCH=8-1-stable
|
|
34
49
|
allow-failure: true
|
|
35
|
-
- ruby-version:
|
|
36
|
-
active-record
|
|
50
|
+
- ruby-version: "4.0"
|
|
51
|
+
active-record:
|
|
52
|
+
label: AR 8-0-stable
|
|
53
|
+
env: ACTIVE_RECORD_BRANCH=8-0-stable
|
|
37
54
|
allow-failure: true
|
|
38
|
-
- ruby-version:
|
|
39
|
-
active-record
|
|
55
|
+
- ruby-version: "4.0"
|
|
56
|
+
active-record:
|
|
57
|
+
label: AR 7-2-stable
|
|
58
|
+
env: ACTIVE_RECORD_BRANCH=7-2-stable
|
|
40
59
|
allow-failure: true
|
|
41
|
-
|
|
42
|
-
|
|
60
|
+
# A canary for the next Ruby, which cannot fail the build.
|
|
61
|
+
- ruby-version: ruby-head
|
|
62
|
+
active-record:
|
|
63
|
+
label: AR 8.1
|
|
64
|
+
env: ACTIVE_RECORD_VERSION=~> 8.1.0
|
|
43
65
|
allow-failure: true
|
|
44
66
|
continue-on-error: ${{ matrix.allow-failure }}
|
|
45
67
|
steps:
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
68
|
+
- uses: actions/checkout@v7
|
|
69
|
+
# Set the version here rather than prefixing each command, so the matrix
|
|
70
|
+
# value reaches the environment without being interpolated into a script.
|
|
71
|
+
- name: Select Active Record version
|
|
72
|
+
env:
|
|
73
|
+
ACTIVE_RECORD_ENV: ${{ matrix.active-record.env }}
|
|
74
|
+
run: echo "$ACTIVE_RECORD_ENV" >> "$GITHUB_ENV"
|
|
75
|
+
- name: Set up Ruby
|
|
76
|
+
uses: ruby/setup-ruby@v1
|
|
77
|
+
with:
|
|
78
|
+
ruby-version: ${{ matrix.ruby-version }}
|
|
79
|
+
bundler-cache: true
|
|
80
|
+
cache-version: ${{ matrix.active-record.env }}
|
|
81
|
+
# Gemfile.lock is not checked in, so this resolves the newest gems that
|
|
82
|
+
# satisfy the Gemfile on each run rather than a recorded set. `--all` is
|
|
83
|
+
# what Bundler 4, shipped with Ruby 4.0, wants for that.
|
|
84
|
+
- name: Update bundle
|
|
85
|
+
run: bundle update --all
|
|
86
|
+
# Not bin/rake: bin/ holds generated binstubs and is not checked in, and
|
|
87
|
+
# whether bundling writes them there varies by Bundler version - Bundler 4
|
|
88
|
+
# does not, which left Ruby 4.0 with no binstub to run.
|
|
89
|
+
- name: Run tests
|
|
90
|
+
run: bundle exec rake
|
data/CHANGELOG.md
CHANGED
|
@@ -1,3 +1,22 @@
|
|
|
1
|
+
### 2.3.0
|
|
2
|
+
|
|
3
|
+
- Add `table(false)` to create no table, so that a model whose superclass is a
|
|
4
|
+
concrete Active Record class uses single table inheritance.
|
|
5
|
+
- Accept a String, a Symbol, or a callable for `superclass:`, resolved for each
|
|
6
|
+
example, so that another `with_model` model can be the superclass.
|
|
7
|
+
- Raise `WithModel::InvalidSuperclass` for a `superclass:` that cannot be used,
|
|
8
|
+
and `WithModel::MissingSuperclass` for a name that resolves to nothing. Both
|
|
9
|
+
are `ArgumentError`s, and the latter is an `InvalidSuperclass`, so either can
|
|
10
|
+
be rescued as narrowly as needed.
|
|
11
|
+
- Deprecate omitting `table`, which currently creates a table with only an id
|
|
12
|
+
column. In 3.0 it will create no table.
|
|
13
|
+
- Create minitest models in the order they are declared, so that a model can
|
|
14
|
+
refer to another model declared above it.
|
|
15
|
+
- Add support for Active Record 8.1.
|
|
16
|
+
- Require Active Record 7.2 or later.
|
|
17
|
+
- Add support for Ruby 4.0.
|
|
18
|
+
- Require Ruby 3.3 or later.
|
|
19
|
+
|
|
1
20
|
### 2.2.0
|
|
2
21
|
|
|
3
22
|
- Fix dependency tracking issue when `cache_classes: true` is set in Rails 7+.
|
data/CONTRIBUTING.md
ADDED
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
# Contributing to with_model
|
|
2
|
+
|
|
3
|
+
If you experience a bug, we welcome you to report it. Please include a minimal
|
|
4
|
+
example showing the code you ran, what happened, and what you expected to happen
|
|
5
|
+
instead — a failing `with_model` block is ideal, since it can be dropped straight
|
|
6
|
+
into the suite. If you can fix the bug and open a pull request, it will get
|
|
7
|
+
resolved sooner, but don't hesitate to report an issue you don't know how to fix.
|
|
8
|
+
|
|
9
|
+
If you have a substantial feature in mind, consider opening an issue to discuss
|
|
10
|
+
it first. We may have thought about it already, and can sometimes save you a
|
|
11
|
+
detour.
|
|
12
|
+
|
|
13
|
+
When in doubt, go ahead and open a pull request. If something needs rethinking,
|
|
14
|
+
we will do our best to say so clearly. Don't be discouraged if we ask you to
|
|
15
|
+
change your code — we appreciate the work, and we also have opinions about style
|
|
16
|
+
and object design.
|
|
17
|
+
|
|
18
|
+
## Supported versions
|
|
19
|
+
|
|
20
|
+
The [gemspec](./with_model.gemspec) declares the minimum supported Ruby and
|
|
21
|
+
Active Record, and the [CI workflow](./.github/workflows/ci.yml) lists every
|
|
22
|
+
combination actually tested. It can be hard to try them all locally, so please
|
|
23
|
+
avoid anything that only works on the newest of either.
|
|
24
|
+
|
|
25
|
+
## Running the tests
|
|
26
|
+
|
|
27
|
+
The suite runs against both supported test harnesses, so `with_model` has to work
|
|
28
|
+
under each:
|
|
29
|
+
|
|
30
|
+
- `spec/` covers behavior under RSpec.
|
|
31
|
+
- `test/` covers the minitest life cycle — the setup and teardown hooks, and
|
|
32
|
+
their ordering — rather than repeating the specs.
|
|
33
|
+
|
|
34
|
+
Run everything, including the linter, with:
|
|
35
|
+
|
|
36
|
+
```sh
|
|
37
|
+
bundle exec rake
|
|
38
|
+
```
|
|
39
|
+
|
|
40
|
+
`bin/` holds generated binstubs and is not checked in. Whether bundling writes
|
|
41
|
+
them for you depends on your Bundler version, so create them once if you would
|
|
42
|
+
rather type `bin/rake`:
|
|
43
|
+
|
|
44
|
+
```sh
|
|
45
|
+
bundle binstubs --all
|
|
46
|
+
```
|
|
47
|
+
|
|
48
|
+
Our automated tests begin by updating every gem to its newest version. That is
|
|
49
|
+
deliberate: we would rather find out about an incompatibility from our own build
|
|
50
|
+
than from a bug report. `Gemfile.lock` is not checked in, so you can do the same
|
|
51
|
+
at any time with `bundle update --all`.
|
|
52
|
+
|
|
53
|
+
To try a particular Active Record, set `ACTIVE_RECORD_VERSION` to a version
|
|
54
|
+
requirement and update the bundle:
|
|
55
|
+
|
|
56
|
+
```sh
|
|
57
|
+
ACTIVE_RECORD_VERSION="~> 8.1.0" bundle update --all
|
|
58
|
+
ACTIVE_RECORD_VERSION="~> 8.1.0" bundle exec rake
|
|
59
|
+
```
|
|
60
|
+
|
|
61
|
+
To test against Active Record from git instead — an unreleased branch, or `main`
|
|
62
|
+
— set `ACTIVE_RECORD_BRANCH`:
|
|
63
|
+
|
|
64
|
+
```sh
|
|
65
|
+
ACTIVE_RECORD_BRANCH=main bundle update --all
|
|
66
|
+
ACTIVE_RECORD_BRANCH=main bundle exec rake
|
|
67
|
+
```
|
|
68
|
+
|
|
69
|
+
Style is enforced by [Standard](https://github.com/standardrb/standard), which
|
|
70
|
+
`rake` runs. To correct what it can:
|
|
71
|
+
|
|
72
|
+
```sh
|
|
73
|
+
bundle exec standardrb --fix
|
|
74
|
+
```
|
|
75
|
+
|
|
76
|
+
## Changes and releases
|
|
77
|
+
|
|
78
|
+
Please add a line to `CHANGELOG.md` under `### Unreleased` describing your change
|
|
79
|
+
from the point of view of someone using the gem. Leave `lib/with_model/version.rb`
|
|
80
|
+
alone: version bumps and releases are cut separately by a maintainer, so a bump
|
|
81
|
+
in a pull request only creates a conflict.
|
|
82
|
+
|
|
83
|
+
Last, but not least, have fun.
|
data/Gemfile
CHANGED
|
@@ -9,7 +9,6 @@ ar_version = ENV.fetch("ACTIVE_RECORD_VERSION", nil)
|
|
|
9
9
|
|
|
10
10
|
if ar_branch
|
|
11
11
|
gem "activerecord", git: "https://github.com/rails/rails.git", branch: ar_branch
|
|
12
|
-
gem "arel", git: "https://github.com/rails/arel.git" if ar_branch == "master"
|
|
13
12
|
elsif ar_version
|
|
14
13
|
gem "activerecord", ar_version
|
|
15
14
|
end
|
|
@@ -22,10 +21,6 @@ gem "minitest"
|
|
|
22
21
|
gem "mutex_m"
|
|
23
22
|
gem "rake"
|
|
24
23
|
gem "rspec"
|
|
24
|
+
gem "ruby-lsp"
|
|
25
|
+
gem "sqlite3"
|
|
25
26
|
gem "standard"
|
|
26
|
-
|
|
27
|
-
if ar_branch == "7-0-stable" || ar_version == "~> 7.0.0"
|
|
28
|
-
gem "sqlite3", "< 2"
|
|
29
|
-
else
|
|
30
|
-
gem "sqlite3"
|
|
31
|
-
end
|
data/LICENSE
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
Copyright (c) 2010–
|
|
1
|
+
Copyright (c) 2010–2026 Casebook, PBC <http://www.casebook.net>
|
|
2
2
|
|
|
3
3
|
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
4
4
|
of this software and associated documentation files (the "Software"), to deal
|
data/README.md
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
# [with_model](https://github.com/Casecommons/with_model)
|
|
2
2
|
|
|
3
3
|
[](https://rubygems.org/gems/with_model)
|
|
4
|
-
[](https://github.com/Casecommons/with_model/actions/workflows/ci.yml)
|
|
4
|
+
[](https://github.com/Casecommons/with_model/actions/workflows/ci.yml?query=branch%3Amaster)
|
|
5
5
|
[](https://www.rubydoc.info/gems/with_model)
|
|
6
6
|
|
|
7
7
|
`with_model` dynamically builds an Active Record model (with table) before each test in a group and destroys it afterwards.
|
|
@@ -19,7 +19,7 @@ Install as usual: `gem install with_model` or add `gem 'with_model'` to your Gem
|
|
|
19
19
|
Extend `WithModel` into RSpec:
|
|
20
20
|
|
|
21
21
|
```ruby
|
|
22
|
-
require
|
|
22
|
+
require "with_model"
|
|
23
23
|
|
|
24
24
|
RSpec.configure do |config|
|
|
25
25
|
config.extend WithModel
|
|
@@ -31,7 +31,7 @@ end
|
|
|
31
31
|
Extend `WithModel` into minitest/spec and set the test runner explicitly:
|
|
32
32
|
|
|
33
33
|
```ruby
|
|
34
|
-
require
|
|
34
|
+
require "with_model"
|
|
35
35
|
|
|
36
36
|
WithModel.runner = :minitest
|
|
37
37
|
|
|
@@ -45,11 +45,16 @@ end
|
|
|
45
45
|
After setting up as above, call `with_model` and inside its block pass it a `table` block and a `model` block.
|
|
46
46
|
|
|
47
47
|
```ruby
|
|
48
|
-
require
|
|
48
|
+
require "spec_helper"
|
|
49
49
|
|
|
50
|
-
|
|
51
|
-
|
|
50
|
+
module MyModule; end
|
|
51
|
+
|
|
52
|
+
# A pre-existing model
|
|
53
|
+
class Car < ActiveRecord::Base
|
|
54
|
+
self.abstract_class = true
|
|
55
|
+
end
|
|
52
56
|
|
|
57
|
+
describe "A blog post" do
|
|
53
58
|
with_model :BlogPost do
|
|
54
59
|
# The table block (and an options hash) is passed to Active Record migration’s `create_table`.
|
|
55
60
|
table do |t|
|
|
@@ -60,15 +65,16 @@ describe "A blog post" do
|
|
|
60
65
|
# The model block is the Active Record model’s class body.
|
|
61
66
|
model do
|
|
62
67
|
include MyModule
|
|
68
|
+
|
|
63
69
|
has_many :comments
|
|
64
70
|
validates_presence_of :title
|
|
65
71
|
|
|
66
72
|
def self.some_class_method
|
|
67
|
-
|
|
73
|
+
"chunky"
|
|
68
74
|
end
|
|
69
75
|
|
|
70
76
|
def some_instance_method
|
|
71
|
-
|
|
77
|
+
"bacon"
|
|
72
78
|
end
|
|
73
79
|
end
|
|
74
80
|
end
|
|
@@ -91,15 +97,15 @@ describe "A blog post" do
|
|
|
91
97
|
end
|
|
92
98
|
|
|
93
99
|
it "has the module" do
|
|
94
|
-
expect(BlogPost.include?(MyModule)).to
|
|
100
|
+
expect(BlogPost.include?(MyModule)).to be true
|
|
95
101
|
end
|
|
96
102
|
|
|
97
103
|
it "has the class method" do
|
|
98
|
-
expect(BlogPost.some_class_method).to eq
|
|
104
|
+
expect(BlogPost.some_class_method).to eq "chunky"
|
|
99
105
|
end
|
|
100
106
|
|
|
101
107
|
it "has the instance method" do
|
|
102
|
-
expect(BlogPost.new.some_instance_method).to eq
|
|
108
|
+
expect(BlogPost.new.some_instance_method).to eq "bacon"
|
|
103
109
|
end
|
|
104
110
|
|
|
105
111
|
it "can do all the things a regular model can" do
|
|
@@ -107,22 +113,21 @@ describe "A blog post" do
|
|
|
107
113
|
expect(record).not_to be_valid
|
|
108
114
|
record.title = "foo"
|
|
109
115
|
expect(record).to be_valid
|
|
110
|
-
expect(record.save).to
|
|
116
|
+
expect(record.save).to be true
|
|
111
117
|
expect(record.reload).to eq record
|
|
112
|
-
record.comments.create!(:
|
|
118
|
+
record.comments.create!(text: "Lorem ipsum")
|
|
113
119
|
expect(record.comments.count).to eq 1
|
|
114
120
|
end
|
|
115
121
|
|
|
116
|
-
# with_model classes can have inheritance.
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
end
|
|
120
|
-
|
|
122
|
+
# with_model classes can have inheritance. Car is abstract, so it has no table
|
|
123
|
+
# and Ford gets one of its own. To inherit a concrete superclass's table
|
|
124
|
+
# instead, see "Single table inheritance" below.
|
|
121
125
|
with_model :Ford, superclass: Car do
|
|
126
|
+
table
|
|
122
127
|
end
|
|
123
128
|
|
|
124
129
|
it "has a specified superclass" do
|
|
125
|
-
expect(Ford
|
|
130
|
+
expect(Ford.new).to be_a(Car)
|
|
126
131
|
end
|
|
127
132
|
end
|
|
128
133
|
|
|
@@ -150,8 +155,8 @@ end
|
|
|
150
155
|
|
|
151
156
|
describe "with table options" do
|
|
152
157
|
with_model :WithOptions do
|
|
153
|
-
table :
|
|
154
|
-
t.string
|
|
158
|
+
table id: false do |t|
|
|
159
|
+
t.string "foo"
|
|
155
160
|
t.timestamps null: false
|
|
156
161
|
end
|
|
157
162
|
end
|
|
@@ -162,6 +167,89 @@ describe "with table options" do
|
|
|
162
167
|
end
|
|
163
168
|
```
|
|
164
169
|
|
|
170
|
+
## Single table inheritance
|
|
171
|
+
|
|
172
|
+
Pass `table(false)` to create no table at all. Active Record's own inheritance
|
|
173
|
+
then supplies the superclass's table, which is what single table inheritance
|
|
174
|
+
needs.
|
|
175
|
+
|
|
176
|
+
The superclass can be another `with_model` model. Its constant does not exist yet
|
|
177
|
+
when the `superclass:` argument is read, so name it with a String or a Symbol, or
|
|
178
|
+
pass a callable returning it, and it will be resolved once per example.
|
|
179
|
+
|
|
180
|
+
```ruby
|
|
181
|
+
describe "with_model supports Single Table Inheritance" do
|
|
182
|
+
with_model :Sandwich do
|
|
183
|
+
table do |t|
|
|
184
|
+
t.string "type"
|
|
185
|
+
t.string "bread"
|
|
186
|
+
end
|
|
187
|
+
end
|
|
188
|
+
|
|
189
|
+
with_model :ChunkyBacon, superclass: :Sandwich do
|
|
190
|
+
table(false)
|
|
191
|
+
end
|
|
192
|
+
|
|
193
|
+
it "shares the superclass's table" do
|
|
194
|
+
expect(ChunkyBacon.table_name).to eq Sandwich.table_name
|
|
195
|
+
end
|
|
196
|
+
|
|
197
|
+
it "stores its own type" do
|
|
198
|
+
sandwich = ChunkyBacon.create!(bread: "rye")
|
|
199
|
+
|
|
200
|
+
expect(sandwich.reload.type).to eq "ChunkyBacon"
|
|
201
|
+
expect(Sandwich.first).to be_a ChunkyBacon
|
|
202
|
+
end
|
|
203
|
+
end
|
|
204
|
+
```
|
|
205
|
+
|
|
206
|
+
A `superclass:` that cannot be used raises `WithModel::InvalidSuperclass`: one
|
|
207
|
+
that is not an Active Record class, one with no table of its own
|
|
208
|
+
(`ActiveRecord::Base`, or an abstract class such as a Rails app's
|
|
209
|
+
`ApplicationRecord`), or one whose table has no inheritance column to tell a
|
|
210
|
+
subclass's rows apart. A name that resolves to nothing raises
|
|
211
|
+
`WithModel::MissingSuperclass`, a kind of `InvalidSuperclass`, quoting the
|
|
212
|
+
constant Ruby could not find — for a namespaced name, that is the segment which
|
|
213
|
+
is actually missing. Both are `ArgumentError`s.
|
|
214
|
+
|
|
215
|
+
A superclass whose table does not exist *yet* is allowed, since the table may be
|
|
216
|
+
created later in the example, and Active Record reports its absence clearly
|
|
217
|
+
enough on its own. Rows the model wrote are deleted when it goes away, since they
|
|
218
|
+
name a class that is about to stop existing and would make the superclass
|
|
219
|
+
unloadable.
|
|
220
|
+
|
|
221
|
+
## Foreign keys
|
|
222
|
+
|
|
223
|
+
`foreign_key: true` asks Active Record to infer the table a foreign key points
|
|
224
|
+
at, and it infers `authors` from `author_id`. Generated table names are unique
|
|
225
|
+
rather than conventional, so name the table instead:
|
|
226
|
+
|
|
227
|
+
```ruby
|
|
228
|
+
describe "with_model supports foreign keys" do
|
|
229
|
+
with_model :Author do
|
|
230
|
+
table
|
|
231
|
+
end
|
|
232
|
+
|
|
233
|
+
with_model :Book do
|
|
234
|
+
table do |t|
|
|
235
|
+
t.references :author, foreign_key: {to_table: Author.table_name}
|
|
236
|
+
end
|
|
237
|
+
|
|
238
|
+
model do
|
|
239
|
+
belongs_to :author
|
|
240
|
+
end
|
|
241
|
+
end
|
|
242
|
+
|
|
243
|
+
it "has a foreign key" do
|
|
244
|
+
expect { Book.create!(author_id: 0) }
|
|
245
|
+
.to raise_error ActiveRecord::InvalidForeignKey
|
|
246
|
+
end
|
|
247
|
+
end
|
|
248
|
+
```
|
|
249
|
+
|
|
250
|
+
This reads `Author.table_name`, so declare `Author` first: models are created in
|
|
251
|
+
the order they are declared, and destroyed in the reverse order.
|
|
252
|
+
|
|
165
253
|
## Requirements
|
|
166
254
|
|
|
167
255
|
See the [gemspec metadata](https://rubygems.org/gems/with_model) for dependency requirements. RSpec and minitest are indirect dependencies, and `with_model` should support any maintained version of both.
|
|
@@ -177,11 +265,17 @@ See the [gemspec metadata](https://rubygems.org/gems/with_model) for dependency
|
|
|
177
265
|
|
|
178
266
|
In general, `with_model` is not guaranteed to be thread-safe, but is, in certain usages, safe to use concurrently across multiple processes with a single database schema.
|
|
179
267
|
|
|
268
|
+
## Contributing
|
|
269
|
+
|
|
270
|
+
Bug reports and pull requests are welcome. See the
|
|
271
|
+
[CONTRIBUTING guide](/CONTRIBUTING.md) for how to run the tests against a
|
|
272
|
+
particular Ruby or Active Record.
|
|
273
|
+
|
|
180
274
|
## Versioning
|
|
181
275
|
|
|
182
276
|
`with_model` uses [Semantic Versioning 2.0.0](http://semver.org/spec/v2.0.0.html).
|
|
183
277
|
|
|
184
278
|
## License
|
|
185
279
|
|
|
186
|
-
Copyright © 2010–
|
|
280
|
+
Copyright © 2010–2026 [Casebook PBC](https://www.casebook.net).
|
|
187
281
|
Licensed under the MIT license, see [LICENSE](/LICENSE) file.
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module WithModel
|
|
4
|
+
# Raised when the `superclass:` a model was given cannot be used: it is not an
|
|
5
|
+
# Active Record class at all, or it is one that cannot supply a table to a model
|
|
6
|
+
# which has none of its own - because it has no table itself
|
|
7
|
+
# (`ActiveRecord::Base`, or an abstract class), or because its table has no
|
|
8
|
+
# inheritance column and so cannot tell a subclass's rows apart.
|
|
9
|
+
#
|
|
10
|
+
# An `ArgumentError`, because each is a fact about the argument rather than about
|
|
11
|
+
# when it was looked at, and the superclass is needed the moment the model is
|
|
12
|
+
# built - unlike a table, which an example is free to create later.
|
|
13
|
+
class InvalidSuperclass < ArgumentError
|
|
14
|
+
end
|
|
15
|
+
end
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "with_model/invalid_superclass"
|
|
4
|
+
|
|
5
|
+
module WithModel
|
|
6
|
+
# Raised when the name given for a `superclass:` resolves to nothing at all.
|
|
7
|
+
#
|
|
8
|
+
# A kind of {WithModel::InvalidSuperclass}, so every superclass that cannot be
|
|
9
|
+
# used is catchable in one place, while a name that is simply not there - a
|
|
10
|
+
# typo, or a with_model superclass declared after the models inheriting it -
|
|
11
|
+
# can be told apart from a class that exists but cannot supply a table.
|
|
12
|
+
class MissingSuperclass < InvalidSuperclass
|
|
13
|
+
end
|
|
14
|
+
end
|
data/lib/with_model/model/dsl.rb
CHANGED
|
@@ -11,10 +11,13 @@ module WithModel
|
|
|
11
11
|
# Provide a schema definition for the table, passed to ActiveRecord's `create_table`.
|
|
12
12
|
# The table name will be auto-generated.
|
|
13
13
|
#
|
|
14
|
+
# Pass `false` instead of options to create no table at all, so that the
|
|
15
|
+
# model inherits its superclass's table as Rails Single Table Inheritance
|
|
16
|
+
# requires. `table(false)` takes no block.
|
|
17
|
+
#
|
|
14
18
|
# @see https://api.rubyonrails.org/classes/ActiveRecord/ConnectionAdapters/SchemaStatements.html#method-i-create_table
|
|
15
19
|
def table(options = {}, &block)
|
|
16
|
-
@model.
|
|
17
|
-
@model.table_block = block
|
|
20
|
+
@model.specify_table(options, block)
|
|
18
21
|
end
|
|
19
22
|
|
|
20
23
|
# Provide a class body for the ActiveRecord model.
|