with_model 2.1.7 → 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 +67 -21
- data/.rspec +1 -1
- data/CHANGELOG.md +29 -0
- data/CONTRIBUTING.md +83 -0
- data/Gemfile +16 -23
- data/LICENSE +1 -1
- data/README.md +116 -22
- data/Rakefile +8 -17
- data/lib/with_model/constant_stubber.rb +1 -1
- data/lib/with_model/descendants_tracker.rb +87 -0
- 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 +97 -21
- data/lib/with_model/null_table.rb +98 -0
- data/lib/with_model/table.rb +18 -6
- data/lib/with_model/version.rb +1 -1
- data/lib/with_model.rb +45 -7
- data/spec/active_record_behaviors_spec.rb +31 -28
- data/spec/constant_stubber_spec.rb +5 -5
- data/spec/descendants_tracking_spec.rb +49 -0
- data/spec/readme_spec.rb +82 -33
- data/spec/spec_helper.rb +8 -18
- data/spec/table_false_spec.rb +516 -0
- data/spec/with_model_spec.rb +138 -124
- 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 +9 -18
- data/test/with_model_test.rb +9 -9
- data/with_model.gemspec +15 -15
- metadata +16 -12
- data/.rubocop.yml +0 -33
- data/.rubocop_todo.yml +0 -78
- data/spec/.rubocop.yml +0 -5
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
require "active_support/descendants_tracker"
|
|
2
|
+
|
|
3
|
+
module WithModel
|
|
4
|
+
# Based on https://github.com/rails/rails/blob/491afff27e2dd3d5f301b478b9a43d3c31709af8/activesupport/lib/active_support/descendants_tracker.rb
|
|
5
|
+
module DescendantsTracker
|
|
6
|
+
if RUBY_ENGINE == "ruby"
|
|
7
|
+
# On MRI `ObjectSpace::WeakMap` keys are weak references.
|
|
8
|
+
# So we can simply use WeakMap as a `Set`.
|
|
9
|
+
class WeakSet < ObjectSpace::WeakMap # :nodoc:
|
|
10
|
+
alias_method :to_a, :keys
|
|
11
|
+
|
|
12
|
+
def <<(object)
|
|
13
|
+
self[object] = true
|
|
14
|
+
end
|
|
15
|
+
end
|
|
16
|
+
else
|
|
17
|
+
# On TruffleRuby `ObjectSpace::WeakMap` keys are strong references.
|
|
18
|
+
# So we use `object_id` as a key and the actual object as a value.
|
|
19
|
+
#
|
|
20
|
+
# JRuby for now doesn't have Class#descendant, but when it will, it will likely
|
|
21
|
+
# have the same WeakMap semantic than Truffle so we future proof this as much as possible.
|
|
22
|
+
class WeakSet # :nodoc:
|
|
23
|
+
def initialize
|
|
24
|
+
@map = ObjectSpace::WeakMap.new
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
def [](object)
|
|
28
|
+
@map.key?(object.object_id)
|
|
29
|
+
end
|
|
30
|
+
alias_method :include?, :[]
|
|
31
|
+
|
|
32
|
+
def []=(object, _present)
|
|
33
|
+
@map[object.object_id] = object
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
def to_a
|
|
37
|
+
@map.values
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
def <<(object)
|
|
41
|
+
self[object] = true
|
|
42
|
+
end
|
|
43
|
+
end
|
|
44
|
+
end
|
|
45
|
+
@excluded_descendants = WeakSet.new
|
|
46
|
+
|
|
47
|
+
class << self
|
|
48
|
+
def clear(classes) # :nodoc:
|
|
49
|
+
classes.each do |klass|
|
|
50
|
+
@excluded_descendants << klass
|
|
51
|
+
klass.descendants.each do |descendant|
|
|
52
|
+
@excluded_descendants << descendant
|
|
53
|
+
end
|
|
54
|
+
end
|
|
55
|
+
end
|
|
56
|
+
|
|
57
|
+
def reject!(classes) # :nodoc:
|
|
58
|
+
if @excluded_descendants
|
|
59
|
+
classes.reject! { |d| @excluded_descendants.include?(d) }
|
|
60
|
+
end
|
|
61
|
+
classes
|
|
62
|
+
end
|
|
63
|
+
end
|
|
64
|
+
|
|
65
|
+
module DestroyedClassesFiltering
|
|
66
|
+
def subclasses
|
|
67
|
+
WithModel::DescendantsTracker.reject!(super)
|
|
68
|
+
end
|
|
69
|
+
|
|
70
|
+
def descendants
|
|
71
|
+
WithModel::DescendantsTracker.reject!(super)
|
|
72
|
+
end
|
|
73
|
+
end
|
|
74
|
+
end
|
|
75
|
+
end
|
|
76
|
+
|
|
77
|
+
class ActiveRecord::Base
|
|
78
|
+
extend WithModel::DescendantsTracker::DestroyedClassesFiltering
|
|
79
|
+
end
|
|
80
|
+
|
|
81
|
+
module ActiveSupport
|
|
82
|
+
module DescendantsTracker
|
|
83
|
+
class << self
|
|
84
|
+
attr_reader :clear_disabled
|
|
85
|
+
end
|
|
86
|
+
end
|
|
87
|
+
end
|
|
@@ -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.
|
data/lib/with_model/model.rb
CHANGED
|
@@ -1,11 +1,16 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
-
require
|
|
4
|
-
require
|
|
5
|
-
require
|
|
6
|
-
require
|
|
7
|
-
require
|
|
8
|
-
require
|
|
3
|
+
require "logger"
|
|
4
|
+
require "active_record"
|
|
5
|
+
require "active_support/core_ext/string/inflections"
|
|
6
|
+
require "English"
|
|
7
|
+
require "with_model/constant_stubber"
|
|
8
|
+
require "with_model/descendants_tracker"
|
|
9
|
+
require "with_model/methods"
|
|
10
|
+
require "with_model/invalid_superclass"
|
|
11
|
+
require "with_model/missing_superclass"
|
|
12
|
+
require "with_model/null_table"
|
|
13
|
+
require "with_model/table"
|
|
9
14
|
|
|
10
15
|
module WithModel
|
|
11
16
|
# In general, direct use of this class should be avoided. Instead use
|
|
@@ -14,17 +19,43 @@ module WithModel
|
|
|
14
19
|
attr_writer :model_block, :table_block, :table_options
|
|
15
20
|
|
|
16
21
|
# @param [Symbol] name The constant name to assign the model class to.
|
|
17
|
-
# @param
|
|
18
|
-
#
|
|
22
|
+
# @param superclass The superclass for the created class. Either a Class
|
|
23
|
+
# having `ActiveRecord::Base` as an ancestor, a String naming one, or a
|
|
24
|
+
# callable returning one. A String or callable is resolved afresh for
|
|
25
|
+
# every example, which is what allows another `with_model` model - whose
|
|
26
|
+
# constant does not exist when this line is read - to be the superclass.
|
|
19
27
|
def initialize(name, superclass: ActiveRecord::Base)
|
|
20
28
|
@name = name.to_sym
|
|
21
29
|
@model_block = nil
|
|
22
30
|
@table_block = nil
|
|
23
31
|
@table_options = {}
|
|
24
|
-
@
|
|
32
|
+
@table_specified = false
|
|
33
|
+
@skip_table = false
|
|
34
|
+
@superclass_spec = superclass
|
|
25
35
|
end
|
|
26
36
|
|
|
37
|
+
# Records what {WithModel::Model::DSL#table} was asked for, including the
|
|
38
|
+
# fact that it was asked for at all.
|
|
39
|
+
def specify_table(options, block)
|
|
40
|
+
@table_specified = true
|
|
41
|
+
|
|
42
|
+
if options == false
|
|
43
|
+
raise ArgumentError, "table does not take a block when its first argument is falsy" if block
|
|
44
|
+
|
|
45
|
+
@skip_table = true
|
|
46
|
+
else
|
|
47
|
+
@table_options = options
|
|
48
|
+
@table_block = block
|
|
49
|
+
end
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
# Whether a table was specified at all. A `table` call with no arguments
|
|
53
|
+
# counts, so this cannot be inferred from the options and block alone.
|
|
54
|
+
def table_specified? = @table_specified
|
|
55
|
+
|
|
27
56
|
def create
|
|
57
|
+
@superclass = resolve_superclass
|
|
58
|
+
@table = nil
|
|
28
59
|
table.create
|
|
29
60
|
@model = Class.new(@superclass) do
|
|
30
61
|
extend WithModel::Methods
|
|
@@ -34,35 +65,76 @@ module WithModel
|
|
|
34
65
|
end
|
|
35
66
|
|
|
36
67
|
def destroy
|
|
68
|
+
# Test runners tear down even when setup raised, so `create` may not have
|
|
69
|
+
# reached the point of building the model. Nothing was stubbed and nothing
|
|
70
|
+
# wrote rows, so there is nothing to undo.
|
|
71
|
+
return unless @model
|
|
72
|
+
|
|
73
|
+
# Before `unstub_const`: a teardown that identifies this model's own rows
|
|
74
|
+
# can only do so while the class still has its name.
|
|
75
|
+
table.teardown(@model)
|
|
37
76
|
stubber.unstub_const
|
|
38
77
|
cleanup_descendants_tracking
|
|
39
78
|
reset_dependencies_cache
|
|
40
|
-
|
|
79
|
+
WithModel::DescendantsTracker.clear([@model])
|
|
41
80
|
@model = nil
|
|
42
81
|
end
|
|
43
82
|
|
|
44
83
|
private
|
|
45
84
|
|
|
85
|
+
def resolve_superclass
|
|
86
|
+
spec = @superclass_spec
|
|
87
|
+
spec = spec.call if spec.respond_to?(:call)
|
|
88
|
+
spec = constantize_superclass(spec) if class_name?(spec)
|
|
89
|
+
|
|
90
|
+
unless spec.is_a?(Class) && spec <= ActiveRecord::Base
|
|
91
|
+
raise InvalidSuperclass,
|
|
92
|
+
"superclass must be a Class descending from ActiveRecord::Base, but was #{spec.inspect}. " \
|
|
93
|
+
"To refer to another with_model class, or anything else that is only defined once the " \
|
|
94
|
+
"test is running, name it with a String or a Symbol, or pass a callable returning it."
|
|
95
|
+
end
|
|
96
|
+
|
|
97
|
+
spec
|
|
98
|
+
end
|
|
99
|
+
|
|
100
|
+
# A Symbol reads naturally here, since with_model names its own models with
|
|
101
|
+
# them. Anything else has to answer to `to_str`, which only real strings do:
|
|
102
|
+
# every object has a `to_s`, so accepting that would quietly look up a
|
|
103
|
+
# constant named "42".
|
|
104
|
+
def class_name?(spec)
|
|
105
|
+
spec.is_a?(Symbol) || spec.respond_to?(:to_str)
|
|
106
|
+
end
|
|
107
|
+
|
|
108
|
+
# `to_s` is safe here where `class_name?` has already vouched for the value,
|
|
109
|
+
# and it keeps the Symbol intact for the message below, which reports what was
|
|
110
|
+
# passed in rather than what it was converted to.
|
|
111
|
+
#
|
|
112
|
+
# The NameError is worth quoting rather than replacing: for a namespaced name
|
|
113
|
+
# it reports which segment was missing, which this message cannot work out.
|
|
114
|
+
# Raising inside the rescue leaves it as the `cause` for anything that wants
|
|
115
|
+
# the backtrace.
|
|
116
|
+
def constantize_superclass(name)
|
|
117
|
+
name.to_s.constantize
|
|
118
|
+
rescue NameError => e
|
|
119
|
+
raise MissingSuperclass,
|
|
120
|
+
"superclass #{name.inspect} could not be resolved: #{e.message}. Names are resolved while " \
|
|
121
|
+
"the test is running, so a with_model superclass has to be declared before the models " \
|
|
122
|
+
"that inherit it."
|
|
123
|
+
end
|
|
124
|
+
|
|
46
125
|
def const_name
|
|
47
126
|
@name.to_s.camelize.to_sym
|
|
48
127
|
end
|
|
49
128
|
|
|
50
129
|
def setup_model
|
|
51
|
-
@model
|
|
130
|
+
table.configure(@model)
|
|
52
131
|
@model.class_eval(&@model_block) if @model_block
|
|
53
132
|
@model.reset_column_information
|
|
54
133
|
end
|
|
55
134
|
|
|
56
135
|
def cleanup_descendants_tracking
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
ActiveSupport::DescendantsTracker.clear([@model])
|
|
60
|
-
else
|
|
61
|
-
ActiveSupport::DescendantsTracker.class_variable_get(:@@direct_descendants).delete(ActiveRecord::Base)
|
|
62
|
-
end
|
|
63
|
-
elsif @model.superclass.respond_to?(:direct_descendants)
|
|
64
|
-
@model.superclass.direct_descendants.delete(@model)
|
|
65
|
-
end
|
|
136
|
+
ActiveSupport::DescendantsTracker.clear([@model]) \
|
|
137
|
+
unless ActiveSupport::DescendantsTracker.clear_disabled
|
|
66
138
|
end
|
|
67
139
|
|
|
68
140
|
def reset_dependencies_cache
|
|
@@ -76,7 +148,11 @@ module WithModel
|
|
|
76
148
|
end
|
|
77
149
|
|
|
78
150
|
def table
|
|
79
|
-
@table ||=
|
|
151
|
+
@table ||= if @skip_table
|
|
152
|
+
NullTable.new(@superclass, @name)
|
|
153
|
+
else
|
|
154
|
+
Table.new table_name, @table_options, connection: @superclass.connection, &@table_block
|
|
155
|
+
end
|
|
80
156
|
end
|
|
81
157
|
|
|
82
158
|
def table_name
|
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "active_record"
|
|
4
|
+
require "with_model/invalid_superclass"
|
|
5
|
+
|
|
6
|
+
module WithModel
|
|
7
|
+
# Stands in for a {WithModel::Table} when a model should inherit its
|
|
8
|
+
# superclass's table instead of getting one of its own, as Rails Single Table
|
|
9
|
+
# Inheritance requires. Selected by `table(false)`.
|
|
10
|
+
#
|
|
11
|
+
# In general, direct use of this class should be avoided. Instead use
|
|
12
|
+
# either the {WithModel high-level API} or {WithModel::Model::DSL low-level API}.
|
|
13
|
+
class NullTable
|
|
14
|
+
# @param [Class] superclass The resolved superclass whose table will be
|
|
15
|
+
# inherited.
|
|
16
|
+
# @param [Symbol, String] name The model's name, so that a refusal can say
|
|
17
|
+
# which model it is talking about.
|
|
18
|
+
def initialize(superclass, name)
|
|
19
|
+
@superclass = superclass
|
|
20
|
+
@name = name
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
# Creates nothing, but refuses a superclass that cannot support single
|
|
24
|
+
# table inheritance.
|
|
25
|
+
#
|
|
26
|
+
# Refusals describe the model's situation rather than the call that produced
|
|
27
|
+
# it. Any expression that evaluates to false selects a NullTable, so there
|
|
28
|
+
# is no call spelling to quote - and in with_model 3.0, omitting `table`
|
|
29
|
+
# will arrive here too.
|
|
30
|
+
#
|
|
31
|
+
# A superclass whose table does not exist *yet* is deliberately allowed: the
|
|
32
|
+
# table may be created later in the example, and a test may legitimately
|
|
33
|
+
# want a model whose table is missing. Active Record raises a clear
|
|
34
|
+
# `StatementInvalid` naming the table if it never appears, so refusing here
|
|
35
|
+
# would only forbid working setups.
|
|
36
|
+
#
|
|
37
|
+
# What is left is {WithModel::InvalidSuperclass}: both refusals are permanent
|
|
38
|
+
# facts about the superclass passed in, not about when it was looked at, so no
|
|
39
|
+
# amount of waiting makes them work.
|
|
40
|
+
def create
|
|
41
|
+
refuse "#{@superclass} has none to inherit" unless table_name?
|
|
42
|
+
|
|
43
|
+
# Nothing more can be checked until there is a table to look at.
|
|
44
|
+
return unless table_exists?
|
|
45
|
+
return if inheritance_column?
|
|
46
|
+
|
|
47
|
+
refuse "#{@superclass}'s table #{@superclass.table_name.inspect} has no " \
|
|
48
|
+
"#{inheritance_column.inspect} column, so Active Record cannot tell its rows " \
|
|
49
|
+
"apart from a subclass's"
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
# Deliberately does nothing: leaving `table_name` unassigned is what lets
|
|
53
|
+
# Active Record's own inheritance supply the superclass's table.
|
|
54
|
+
def configure(klass)
|
|
55
|
+
end
|
|
56
|
+
|
|
57
|
+
# Removes the rows this model wrote, which would otherwise outlive the
|
|
58
|
+
# constant that names them and make the superclass unloadable
|
|
59
|
+
# (`ActiveRecord::SubclassNotFound`).
|
|
60
|
+
#
|
|
61
|
+
# `unscoped` is required because a `default_scope` on the superclass would
|
|
62
|
+
# otherwise hide rows from the delete; the inheritance-column condition is
|
|
63
|
+
# then reapplied explicitly, since `unscoped` also discards the type
|
|
64
|
+
# condition that keeps this from touching the superclass's own rows.
|
|
65
|
+
#
|
|
66
|
+
# A table that does not exist holds no rows to remove, and failing here
|
|
67
|
+
# would fail an example whose body had already passed.
|
|
68
|
+
def teardown(klass)
|
|
69
|
+
return unless klass.table_exists?
|
|
70
|
+
|
|
71
|
+
klass.unscoped.where(klass.inheritance_column => klass.sti_name).delete_all
|
|
72
|
+
end
|
|
73
|
+
|
|
74
|
+
# Drops nothing; there is no table of our own to drop.
|
|
75
|
+
def destroy
|
|
76
|
+
end
|
|
77
|
+
|
|
78
|
+
private
|
|
79
|
+
|
|
80
|
+
def inheritance_column = @superclass.inheritance_column
|
|
81
|
+
|
|
82
|
+
# `ActiveRecord::Base` and abstract classes alike have no `table_name`, so
|
|
83
|
+
# this is what "nothing to inherit" actually looks like - `abstract_class?`
|
|
84
|
+
# is false for `ActiveRecord::Base` and so does not describe both.
|
|
85
|
+
def table_name? = @superclass.table_name.present?
|
|
86
|
+
|
|
87
|
+
def table_exists? = @superclass.table_exists?
|
|
88
|
+
|
|
89
|
+
def inheritance_column?
|
|
90
|
+
inheritance_column.present? && @superclass.columns_hash.key?(inheritance_column)
|
|
91
|
+
end
|
|
92
|
+
|
|
93
|
+
def refuse(problem)
|
|
94
|
+
raise InvalidSuperclass,
|
|
95
|
+
"with_model #{@name.inspect} has no table of its own, but #{problem}"
|
|
96
|
+
end
|
|
97
|
+
end
|
|
98
|
+
end
|
data/lib/with_model/table.rb
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
-
require
|
|
3
|
+
require "active_record"
|
|
4
4
|
|
|
5
5
|
module WithModel
|
|
6
6
|
# In general, direct use of this class should be avoided. Instead use
|
|
@@ -8,12 +8,14 @@ module WithModel
|
|
|
8
8
|
class Table
|
|
9
9
|
# @param [Symbol] name The name of the table to create.
|
|
10
10
|
# @param options Passed to ActiveRecord `create_table`.
|
|
11
|
+
# @param connection The connection to use for creating the table.
|
|
11
12
|
# @param block Passed to ActiveRecord `create_table`.
|
|
12
13
|
# @see https://api.rubyonrails.org/classes/ActiveRecord/ConnectionAdapters/SchemaStatements.html#method-i-create_table
|
|
13
|
-
def initialize(name, options = {}, &block)
|
|
14
|
+
def initialize(name, options = {}, connection: ActiveRecord::Base.connection, &block)
|
|
14
15
|
@name = name.freeze
|
|
15
16
|
@options = options.freeze
|
|
16
17
|
@block = block
|
|
18
|
+
@connection = connection
|
|
17
19
|
end
|
|
18
20
|
|
|
19
21
|
# Creates the table with the initialized options. Drops the table if
|
|
@@ -23,12 +25,26 @@ module WithModel
|
|
|
23
25
|
connection.create_table(@name, **@options, &@block)
|
|
24
26
|
end
|
|
25
27
|
|
|
28
|
+
# Points the model at this table.
|
|
29
|
+
def configure(klass)
|
|
30
|
+
klass.table_name = @name
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
# Removes everything this table holds by dropping it. The model is not
|
|
34
|
+
# needed, but is accepted so that {WithModel::NullTable} - which does need
|
|
35
|
+
# it - can stand in here.
|
|
36
|
+
def teardown(_klass)
|
|
37
|
+
destroy
|
|
38
|
+
end
|
|
39
|
+
|
|
26
40
|
def destroy
|
|
27
41
|
connection.drop_table(@name)
|
|
28
42
|
end
|
|
29
43
|
|
|
30
44
|
private
|
|
31
45
|
|
|
46
|
+
attr_reader :connection
|
|
47
|
+
|
|
32
48
|
def exists?
|
|
33
49
|
if connection.respond_to?(:data_source_exists?)
|
|
34
50
|
connection.data_source_exists?(@name)
|
|
@@ -36,9 +52,5 @@ module WithModel
|
|
|
36
52
|
connection.table_exists?(@name)
|
|
37
53
|
end
|
|
38
54
|
end
|
|
39
|
-
|
|
40
|
-
def connection
|
|
41
|
-
ActiveRecord::Base.connection
|
|
42
|
-
end
|
|
43
55
|
end
|
|
44
56
|
end
|
data/lib/with_model/version.rb
CHANGED
data/lib/with_model.rb
CHANGED
|
@@ -1,16 +1,24 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
-
require
|
|
4
|
-
require
|
|
5
|
-
require
|
|
6
|
-
require
|
|
3
|
+
require "active_support/deprecation"
|
|
4
|
+
require "with_model/invalid_superclass"
|
|
5
|
+
require "with_model/missing_superclass"
|
|
6
|
+
require "with_model/model"
|
|
7
|
+
require "with_model/model/dsl"
|
|
8
|
+
require "with_model/null_table"
|
|
9
|
+
require "with_model/table"
|
|
10
|
+
require "with_model/version"
|
|
7
11
|
|
|
8
12
|
module WithModel
|
|
9
13
|
class MiniTestLifeCycle < Module
|
|
10
14
|
def initialize(object)
|
|
15
|
+
# Each with_model includes a fresh module, so the last one declared sits
|
|
16
|
+
# earliest in the ancestor chain. Calling super() first means setup runs
|
|
17
|
+
# in declaration order while teardown unwinds in reverse, which is what
|
|
18
|
+
# lets one with_model refer to another declared above it.
|
|
11
19
|
define_method :before_setup do
|
|
12
|
-
object.create
|
|
13
20
|
super() if defined?(super)
|
|
21
|
+
object.create
|
|
14
22
|
end
|
|
15
23
|
|
|
16
24
|
define_method :after_teardown do
|
|
@@ -32,6 +40,14 @@ module WithModel
|
|
|
32
40
|
@runner ||= :rspec
|
|
33
41
|
end
|
|
34
42
|
|
|
43
|
+
# The deprecator used for with_model's own deprecation warnings. Callers can
|
|
44
|
+
# silence it (`WithModel.deprecator.silenced = true`) or escalate it
|
|
45
|
+
# (`behavior = :raise`) while migrating, and Rails applications can register
|
|
46
|
+
# it in `Rails.application.deprecators`.
|
|
47
|
+
def self.deprecator
|
|
48
|
+
@deprecator ||= ActiveSupport::Deprecation.new("3.0", "with_model")
|
|
49
|
+
end
|
|
50
|
+
|
|
35
51
|
# @param [Symbol] name The constant name to assign the model class to.
|
|
36
52
|
# @param scope Passed to `before`/`after` in the test context. RSpec only.
|
|
37
53
|
# @param options Passed to {WithModel::Model#initialize}.
|
|
@@ -41,6 +57,8 @@ module WithModel
|
|
|
41
57
|
model = Model.new name, **options
|
|
42
58
|
dsl = Model::DSL.new model
|
|
43
59
|
dsl.instance_exec(&block) if block
|
|
60
|
+
# caller_locations(1) is this method's caller: the `with_model` line itself.
|
|
61
|
+
WithModel.warn_omitted_table(name, caller_locations(1)) unless model.table_specified?
|
|
44
62
|
|
|
45
63
|
setup_object(model, scope: scope, runner: runner)
|
|
46
64
|
end
|
|
@@ -56,12 +74,32 @@ module WithModel
|
|
|
56
74
|
setup_object(table, scope: scope, runner: runner)
|
|
57
75
|
end
|
|
58
76
|
|
|
77
|
+
# Warns once per call site, at definition time, rather than once per example.
|
|
78
|
+
# The horizon is stated in the message because `Deprecation#warn` does not
|
|
79
|
+
# interpolate the deprecator's `deprecation_horizon`.
|
|
80
|
+
#
|
|
81
|
+
# The callstack has to be handed in. `ActiveSupport::Deprecation` skips Rails'
|
|
82
|
+
# own frames and the standard library when working out where a warning came
|
|
83
|
+
# from, but with_model's frames look like anyone else's to it, so left to itself
|
|
84
|
+
# it reports this file for every omission in a suite.
|
|
85
|
+
#
|
|
86
|
+
# @param callstack Frames to blame, beginning with the caller to report.
|
|
87
|
+
def self.warn_omitted_table(name, callstack)
|
|
88
|
+
deprecator.warn(
|
|
89
|
+
"with_model #{name.inspect} was called without a `table`, which creates a table with only " \
|
|
90
|
+
"an id column. In with_model 3.0 no table will be created. Call `table` (with no " \
|
|
91
|
+
"arguments or an empty block) to keep a table, or `table(false)` to inherit the " \
|
|
92
|
+
"superclass's table (single table inheritance).",
|
|
93
|
+
callstack
|
|
94
|
+
)
|
|
95
|
+
end
|
|
96
|
+
|
|
59
97
|
private
|
|
60
98
|
|
|
61
99
|
# @param [Object] object The new model object instance to create
|
|
62
100
|
# @param scope Passed to `before`/`after` in the test context. Rspec only.
|
|
63
101
|
# @param [Symbol] runner The test running, either :rspec or :minitest, defaults to :rspec
|
|
64
|
-
def setup_object(object, scope: nil, runner: nil)
|
|
102
|
+
def setup_object(object, scope: nil, runner: nil)
|
|
65
103
|
case runner || WithModel.runner
|
|
66
104
|
when :rspec
|
|
67
105
|
before(*scope) do
|
|
@@ -76,7 +114,7 @@ module WithModel
|
|
|
76
114
|
include MiniTestLifeCycle.call(object)
|
|
77
115
|
end
|
|
78
116
|
else
|
|
79
|
-
raise ArgumentError,
|
|
117
|
+
raise ArgumentError, "Unsupported test runner set, expected :rspec or :minitest"
|
|
80
118
|
end
|
|
81
119
|
end
|
|
82
120
|
end
|