discriminable 2.2.3 → 3.0.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/CHANGELOG.md +32 -0
- data/README.md +86 -24
- data/TODO.md +12 -6
- data/lib/discriminable/version.rb +1 -1
- data/lib/discriminable.rb +48 -33
- metadata +5 -5
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 3f3be0fa32d9828b9d37795c4f04974db45b399b1b729701cc3466bc5136ac2a
|
4
|
+
data.tar.gz: 5e0039a3a78058800f9ae0924146eba632ad3784457c13b448d98cc174b73466
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: ea6a3450967d34c3e275ba4da051463bd1ca9d5a2f79d483edaad6bb5b284bb76bcc498b1ad54b87df62f79728bf1665112a7409c1b621716b813d02201b8c54
|
7
|
+
data.tar.gz: 9cfbe503697d09f795ff4477031ed3afd66e67fc53f01c2a7a226ec6b335eb16ea655fe821ffc4a828afab1c8f2964b16674c47db37491a9619e6a4f3be6720f
|
data/CHANGELOG.md
CHANGED
@@ -1,5 +1,37 @@
|
|
1
1
|
## [Unreleased]
|
2
2
|
|
3
|
+
## [3.0.0](https://github.com/gregorw/discriminable/compare/v2.2.5...v3.0.0) (2022-08-21)
|
4
|
+
|
5
|
+
|
6
|
+
### ⚠ BREAKING CHANGES
|
7
|
+
|
8
|
+
* get rid of legacy hash notation
|
9
|
+
|
10
|
+
### Features
|
11
|
+
|
12
|
+
* Alternate syntax using attribute/value instead of prepositions ([d0843e4](https://github.com/gregorw/discriminable/commit/d0843e4fdf7d9721c29d7259f5c8f0746d345d9b))
|
13
|
+
|
14
|
+
|
15
|
+
### Bug Fixes
|
16
|
+
|
17
|
+
* coverage ([bd862a8](https://github.com/gregorw/discriminable/commit/bd862a835fc0239f0ac59cac1e051293b4b547bc))
|
18
|
+
* get rid of legacy hash notation ([4feda16](https://github.com/gregorw/discriminable/commit/4feda16be37df14dd159b321d3e45a16127fa922))
|
19
|
+
|
20
|
+
### [2.2.5](https://github.com/gregorw/discriminable/compare/v2.2.4...v2.2.5) (2022-04-29)
|
21
|
+
|
22
|
+
|
23
|
+
### Bug Fixes
|
24
|
+
|
25
|
+
* updated description ([d14f366](https://github.com/gregorw/discriminable/commit/d14f366129b31a0b02d8e2314911a45e20ce1f92))
|
26
|
+
|
27
|
+
### [2.2.4](https://github.com/gregorw/discriminable/compare/v2.2.3...v2.2.4) (2022-04-25)
|
28
|
+
|
29
|
+
|
30
|
+
### Bug Fixes
|
31
|
+
|
32
|
+
* changelog url ([3054a1a](https://github.com/gregorw/discriminable/commit/3054a1a0137d8d3cccb55d7f2f06cb392053dab0))
|
33
|
+
* support querying when multiple values are provided ([f75190b](https://github.com/gregorw/discriminable/commit/f75190bcf33dcbddf507626bc77aa4709a594f4d))
|
34
|
+
|
3
35
|
### [2.2.3](https://github.com/gregorw/discriminable/compare/v2.2.2...v2.2.3) (2022-04-20)
|
4
36
|
|
5
37
|
|
data/README.md
CHANGED
@@ -7,24 +7,19 @@
|
|
7
7
|
[](https://codeclimate.com/github/gregorw/discriminable/maintainability)
|
8
8
|
[](https://codeclimate.com/github/gregorw/discriminable/test_coverage)
|
9
9
|
|
10
|
-
|
10
|
+
This is a Ruby gem that implements single-table inheritance (STI) for ActiveRecord models using string, integer and boolean column types.
|
11
11
|
|
12
|
-
In other words, use any
|
13
|
-
|
14
|
-
**Related work**
|
15
|
-
|
16
|
-
The idea was originally described in [“Bye Bye STI, Hello Discriminable Model”](https://www.salsify.com/blog/engineering/bye-bye-sti-hello-discriminable-model) by Randy Burkes and this Gem has started out with [his code](https://gist.github.com/rlburkes/798e186acb2f93e787a5).
|
12
|
+
In other words, it allows to use any (existing) model attribute to discriminate between different subclasses in your class hierarchy. This makes storing class names in a `type` column redundant.
|
17
13
|
|
14
|
+
Also, it supports aliased attributes and _multiple_ values per subclass.
|
18
15
|
|
19
16
|
## Installation
|
20
17
|
|
21
|
-
|
22
|
-
|
23
|
-
$ bundle add discriminable
|
18
|
+
bundle add discriminable
|
24
19
|
|
25
|
-
|
20
|
+
or
|
26
21
|
|
27
|
-
|
22
|
+
gem install discriminable
|
28
23
|
|
29
24
|
## Usage
|
30
25
|
|
@@ -32,41 +27,97 @@ If bundler is not being used to manage dependencies, install the gem by executin
|
|
32
27
|
class Order < ActiveRecord::Base
|
33
28
|
include Discriminable
|
34
29
|
|
35
|
-
|
36
|
-
discriminable state: { open: "Cart" }
|
30
|
+
discriminable_attribute :state
|
37
31
|
end
|
38
32
|
|
39
33
|
class Cart < Order
|
34
|
+
discriminable_value :open
|
40
35
|
end
|
41
36
|
|
42
37
|
Cart.create
|
43
|
-
=> #<Cart id: 1, state: "open">
|
38
|
+
# => #<Cart id: 1, state: "open">
|
44
39
|
Order.all
|
45
|
-
=> #<ActiveRecord::Relation [#<Cart id: 1, state: "open">]>
|
40
|
+
# => #<ActiveRecord::Relation [#<Cart id: 1, state: "open">]>
|
46
41
|
```
|
47
42
|
|
48
|
-
|
43
|
+
## Features
|
49
44
|
|
50
|
-
|
45
|
+
### Compatible with enums
|
51
46
|
|
52
47
|
```ruby
|
53
48
|
class Order < ActiveRecord::Base
|
54
49
|
include Discriminable
|
55
50
|
|
56
|
-
enum state: { open: 0,
|
57
|
-
|
51
|
+
enum state: { open: 0, processing: 1, invoiced: 2 }
|
52
|
+
|
53
|
+
discriminable_attribute :state
|
58
54
|
end
|
59
55
|
|
60
56
|
class Cart < Order
|
61
|
-
|
57
|
+
discriminable_value :open
|
62
58
|
end
|
63
59
|
|
64
|
-
|
65
|
-
|
66
|
-
|
67
|
-
|
60
|
+
class Invoice < Order
|
61
|
+
discriminable_value :invoiced
|
62
|
+
end
|
63
|
+
```
|
64
|
+
|
65
|
+
### Aliased attributes
|
66
|
+
|
67
|
+
In case you are working with a legacy database and cannot change the column name easily it’s easy to reference an aliased attribute in the `discriminable_attribute` definition.
|
68
|
+
|
69
|
+
```ruby
|
70
|
+
class Property < ActiveRecord::Base
|
71
|
+
include Discriminable
|
72
|
+
|
73
|
+
alias_attribute :kind, :kind_with_legacy_postfix
|
74
|
+
|
75
|
+
# Aliased attributes are supported when specifying the discriminable attribute
|
76
|
+
discriminable_attribute :kind
|
77
|
+
end
|
78
|
+
|
79
|
+
class NumberProperty < Property
|
80
|
+
discriminable_value 1
|
81
|
+
end
|
68
82
|
```
|
69
83
|
|
84
|
+
### Multiple values
|
85
|
+
|
86
|
+
Sometimes, in a real project, you may want to map a number of values to a single class. This is possible by specifying:
|
87
|
+
|
88
|
+
```ruby
|
89
|
+
class OptionProperty < Property
|
90
|
+
# The first mention becomes the default value
|
91
|
+
discriminable_values 2, 3, 4
|
92
|
+
end
|
93
|
+
```
|
94
|
+
|
95
|
+
Note that when creating new records with e.g. `OptionProperty.create` a _default_ value needs to be set in the database for this discriminable class. The Discriminable gem uses the _first_ value in the list as the default.
|
96
|
+
|
97
|
+
|
98
|
+
## Comparison with standard Rails
|
99
|
+
|
100
|
+
|
101
|
+
### Rails STI
|
102
|
+
|
103
|
+
| *values* | string | integer | boolean | enum | decimal | … |
|
104
|
+
|--|--|--|--|--|--|--|
|
105
|
+
| single | 🟡 `class.name` only | 🔴 | 🔴 | 🔴 | 🔴 | 🔴 |
|
106
|
+
| multiple | 🔴 | 🔴 | 🔴 | 🔴 | 🔴 | 🔴 |
|
107
|
+
|
108
|
+
### Discriminable Gem
|
109
|
+
|
110
|
+
| *values* | string | integer | boolean | enum | decimal | … |
|
111
|
+
|--|--|--|--|--|--| --|
|
112
|
+
| single | 🟢 | 🟢 | 🟢 | 🟢 | 🟢 | 🟢 |
|
113
|
+
| multiple | 🟢 | 🟢 | 🟢 | 🟢 | 🟢 | 🟢 |
|
114
|
+
|
115
|
+
“Multiple” means that more than one value can map to a single subclass. This may or may not be useful for your use case. In standard Rails, the a single class name obviously maps to a single class.
|
116
|
+
|
117
|
+
## Prerequisits
|
118
|
+
|
119
|
+
Rails 5+ is required.
|
120
|
+
|
70
121
|
|
71
122
|
## Contributing
|
72
123
|
|
@@ -79,3 +130,14 @@ The gem is available as open source under the terms of the [MIT License](https:/
|
|
79
130
|
## Code of Conduct
|
80
131
|
|
81
132
|
Everyone interacting in the Discriminable project's codebases, issue trackers, chat rooms and mailing lists is expected to follow the [code of conduct](https://github.com/gregorw/discriminable/blob/main/CODE_OF_CONDUCT.md).
|
133
|
+
|
134
|
+
## Related work
|
135
|
+
|
136
|
+
The idea for this Gem was influenced by [“Bye Bye STI, Hello Discriminable Model”](https://www.salsify.com/blog/engineering/bye-bye-sti-hello-discriminable-model) by Randy Burkes. This Gem has started out with [his code](https://gist.github.com/rlburkes/798e186acb2f93e787a5).
|
137
|
+
|
138
|
+
See also:
|
139
|
+
|
140
|
+
- Rails [single table inheritance](https://api.rubyonrails.org/classes/ActiveRecord/Inheritance.html) and [DelegatedType](https://api.rubyonrails.org/classes/ActiveRecord/DelegatedType.html)
|
141
|
+
- Java [JPA discrimanator](https://openjpa.apache.org/builds/1.0.2/apache-openjpa-1.0.2/docs/manual/jpa_overview_mapping_discrim.html)
|
142
|
+
- Python [model inheritance](https://docs.djangoproject.com/en/4.0/topics/db/models/#model-inheritance-1)
|
143
|
+
- [Discriminator](https://github.com/gdpelican/discriminator) gem.
|
data/TODO.md
CHANGED
@@ -1,10 +1,16 @@
|
|
1
1
|
- [x] multiple values per subclass
|
2
2
|
- [x] default to first value when using hash syntax
|
3
3
|
- [x] open-closed principle
|
4
|
-
- [
|
5
|
-
- [
|
6
|
-
- [
|
4
|
+
- [x] Bug: multiple values: Child.all query (double-check)
|
5
|
+
- [x] can we use `type` column? => yes
|
6
|
+
- [x] Factory Bot: create :property, kind: 3 => should instantiate a child OptionProperty… => Works if constant OptionProperty is loaded
|
7
|
+
- [x] use `type` column with enum, int, string, etc.
|
8
|
+
- [x] ~~`self.abstract_class = true` ➔ This results in separate tables~~
|
9
|
+
- [x] What if value is not a “discriminable” value and class cannot be found?
|
10
|
+
- [ ] rubocop-minitest
|
11
|
+
- [ ] more tests / examples. [alias, non-alias] ⨉ [integer, string, boolean] ⨉ [enum, non-enum] ⨉ [type-column, non-type-column] ⨉ [multiple-values, single-values] ⨉ [subclasses, subsubclasses] ⨉ [hash-syntax, ocp-syntax]
|
12
|
+
- [ ] Documentation
|
7
13
|
- [ ] test permitted attributes
|
8
|
-
- [ ]
|
9
|
-
- [ ]
|
10
|
-
- [ ]
|
14
|
+
- [ ] scoping… should work OOTB
|
15
|
+
- [ ] At least document `.descendants` issue in Rails development: https://stackoverflow.com/questions/29662518/loading-class-descendants-in-rails-development
|
16
|
+
- [ ] Rails 5 support (see rails-5 branch)
|
data/lib/discriminable.rb
CHANGED
@@ -17,63 +17,78 @@ require "active_support"
|
|
17
17
|
# class Customer < ActiveRecord::Base
|
18
18
|
# include Discriminable
|
19
19
|
#
|
20
|
-
#
|
20
|
+
# discriminable_by :state
|
21
21
|
# end
|
22
22
|
#
|
23
23
|
module Discriminable
|
24
24
|
extend ActiveSupport::Concern
|
25
25
|
|
26
26
|
included do
|
27
|
-
class_attribute :
|
28
|
-
class_attribute :
|
29
|
-
class_attribute :
|
27
|
+
class_attribute :_discriminable_map, instance_writer: false
|
28
|
+
class_attribute :_discriminable_inverse_map, instance_writer: false
|
29
|
+
class_attribute :_discriminable_values, instance_writer: false
|
30
30
|
end
|
31
31
|
|
32
|
-
#
|
32
|
+
# This adds
|
33
|
+
# - `discriminable_attribute` and
|
34
|
+
# - `discriminalbe_value`
|
35
|
+
# class methods (plus some aliases).
|
33
36
|
module ClassMethods
|
34
|
-
|
35
|
-
|
36
|
-
|
37
|
-
attribute, map = options.first
|
38
|
-
|
39
|
-
# E.g. { value: "ClassName" }
|
40
|
-
self.discriminable_map = map.with_indifferent_access
|
37
|
+
# Specify the attribute/column at the root class to use for discrimination.
|
38
|
+
def discriminable_by(attribute)
|
39
|
+
raise "Subclasses should not override .discriminable_by" unless base_class?
|
41
40
|
|
42
|
-
|
43
|
-
|
44
|
-
# { a: "C", b: "C" }.to_a.reverse.to_h.invert => { "C" => :a }
|
45
|
-
# E.g. { "ClassName" => :value }
|
46
|
-
self.discriminable_inverse_map = map.to_a.reverse.to_h.invert
|
41
|
+
self._discriminable_map ||= _discriminable_map_memoized
|
42
|
+
self._discriminable_inverse_map ||= _discriminable_inverse_map_memoized
|
47
43
|
|
48
44
|
attribute = attribute.to_s
|
49
45
|
self.inheritance_column = attribute_aliases[attribute] || attribute
|
50
46
|
end
|
51
47
|
|
52
|
-
|
53
|
-
|
54
|
-
|
55
|
-
|
56
|
-
self.discriminable_inverse_map ||= discriminable_inverse_map_memoized
|
48
|
+
# “Aliases” for discriminable_by
|
49
|
+
def discriminable_attribute(attribute)
|
50
|
+
discriminable_by(attribute)
|
51
|
+
end
|
57
52
|
|
58
|
-
|
59
|
-
|
53
|
+
def discriminable_on(attribute)
|
54
|
+
discriminable_by(attribute)
|
60
55
|
end
|
61
56
|
|
57
|
+
# Specify the values the subclass corresponds to.
|
62
58
|
def discriminable_as(*values)
|
63
59
|
raise "Only subclasses should specify .discriminable_as" if base_class?
|
64
60
|
|
65
|
-
self.
|
61
|
+
self._discriminable_values = values.map do |value|
|
66
62
|
value.instance_of?(Symbol) ? value.to_s : value
|
67
63
|
end
|
68
64
|
end
|
69
65
|
|
66
|
+
def discriminable_value(*values)
|
67
|
+
discriminable_as(*values)
|
68
|
+
end
|
69
|
+
|
70
|
+
def discriminable_values(*values)
|
71
|
+
discriminable_as(*values)
|
72
|
+
end
|
73
|
+
|
70
74
|
# This is the value of the discriminable attribute
|
71
75
|
def sti_name
|
72
|
-
|
76
|
+
_discriminable_inverse_map[name]
|
77
|
+
end
|
78
|
+
|
79
|
+
def sti_names
|
80
|
+
([self] + descendants).flat_map(&:_discriminable_values)
|
81
|
+
end
|
82
|
+
|
83
|
+
def type_condition(table = arel_table)
|
84
|
+
return super unless _discriminable_values.present?
|
85
|
+
|
86
|
+
sti_column = table[inheritance_column]
|
87
|
+
predicate_builder.build(sti_column, sti_names)
|
73
88
|
end
|
74
89
|
|
75
90
|
def sti_class_for(value)
|
76
|
-
return self unless (type_name =
|
91
|
+
return self unless (type_name = _discriminable_map[value])
|
77
92
|
|
78
93
|
super type_name
|
79
94
|
end
|
@@ -85,23 +100,23 @@ module Discriminable
|
|
85
100
|
attrs = attrs.to_h if attrs.respond_to?(:permitted?)
|
86
101
|
return unless attrs.is_a?(Hash)
|
87
102
|
|
88
|
-
value =
|
103
|
+
value = _discriminable_value(attrs)
|
89
104
|
sti_class_for(value)
|
90
105
|
end
|
91
106
|
|
92
|
-
def
|
107
|
+
def _discriminable_map_memoized
|
93
108
|
Hash.new do |map, value|
|
94
|
-
map[value] = descendants.detect { |d| d.
|
109
|
+
map[value] = descendants.detect { |d| d._discriminable_values.include? value }&.name
|
95
110
|
end
|
96
111
|
end
|
97
112
|
|
98
|
-
def
|
113
|
+
def _discriminable_inverse_map_memoized
|
99
114
|
Hash.new do |map, value|
|
100
|
-
map[value] = value.constantize.
|
115
|
+
map[value] = value.constantize._discriminable_values&.first
|
101
116
|
end
|
102
117
|
end
|
103
118
|
|
104
|
-
def
|
119
|
+
def _discriminable_value(attrs)
|
105
120
|
attrs = attrs.with_indifferent_access
|
106
121
|
value = attrs[inheritance_column]
|
107
122
|
value ||= attrs[attribute_aliases.invert[inheritance_column]]
|
metadata
CHANGED
@@ -1,14 +1,14 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: discriminable
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version:
|
4
|
+
version: 3.0.0
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Gregor Wassmann
|
8
8
|
autorequire:
|
9
9
|
bindir: exe
|
10
10
|
cert_chain: []
|
11
|
-
date: 2022-
|
11
|
+
date: 2022-08-21 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
14
|
name: activerecord
|
@@ -108,8 +108,8 @@ dependencies:
|
|
108
108
|
- - "~>"
|
109
109
|
- !ruby/object:Gem::Version
|
110
110
|
version: '1.4'
|
111
|
-
description:
|
112
|
-
|
111
|
+
description: A Ruby gem that implements single-table inheritance (STI) for ActiveRecord
|
112
|
+
models using string, integer and boolean column types.
|
113
113
|
email:
|
114
114
|
- gregor.wassmann@gmail.com
|
115
115
|
executables: []
|
@@ -137,7 +137,7 @@ licenses:
|
|
137
137
|
metadata:
|
138
138
|
homepage_uri: https://github.com/gregorw/discriminable
|
139
139
|
source_code_uri: https://github.com/gregorw/discriminable
|
140
|
-
changelog_uri: https://github.com/gregorw/discriminable/CHANGELOG.md
|
140
|
+
changelog_uri: https://github.com/gregorw/discriminable/blob/main/CHANGELOG.md
|
141
141
|
rubygems_mfa_required: 'false'
|
142
142
|
post_install_message:
|
143
143
|
rdoc_options: []
|