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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 77f7e596256a4d211a4437b0a52d9e0f40d03a038e48c5ce6332083f7f910da9
4
- data.tar.gz: cdaf98658eea3b2faf0bd6193c38fd83d010141c08d7c8cff643f331b51c0986
3
+ metadata.gz: 3f3be0fa32d9828b9d37795c4f04974db45b399b1b729701cc3466bc5136ac2a
4
+ data.tar.gz: 5e0039a3a78058800f9ae0924146eba632ad3784457c13b448d98cc174b73466
5
5
  SHA512:
6
- metadata.gz: eabc2a7dd9bbbce30010f777ffc782e9f875f4c9b5c67daa2d762c224a4493b6f48d74a52b43696de29f0ef2de57128c52e2e83612362e78f896645594e5ef95
7
- data.tar.gz: da4b9c0a03acca87af217b97d996077d8d60e79fe43bf1dc0baaacde5c465b3911ce0259422382c0c9ec0a3aaeea70adaf738aa62d36f1401c0763d3f6f4fa44
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
  [![Maintainability](https://api.codeclimate.com/v1/badges/94041c5f946b64040368/maintainability)](https://codeclimate.com/github/gregorw/discriminable/maintainability)
8
8
  [![Test Coverage](https://api.codeclimate.com/v1/badges/94041c5f946b64040368/test_coverage)](https://codeclimate.com/github/gregorw/discriminable/test_coverage)
9
9
 
10
- Single table inheritance (STI) for Ruby on Rails models (ActiveRecord) using enum, boolean, string and integer column types.
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 _existing_ model attribute for STI instead of storing class names in a `type` column.
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
- Install the gem and add to the application's Gemfile by executing:
22
-
23
- $ bundle add discriminable
18
+ bundle add discriminable
24
19
 
25
- If bundler is not being used to manage dependencies, install the gem by executing:
20
+ or
26
21
 
27
- $ gem install discriminable
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
- enum state: { open: 0, completed: 1 }
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
- ### Alternative syntax
43
+ ## Features
49
44
 
50
- Instead of defining subclass names within the parent you may prefer to be open for extension but closed for modification (open-closed principle) using the alternative syntax.
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, completed: 1 }
57
- discriminable_by :state
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
- discriminable_as :open
57
+ discriminable_value :open
62
58
  end
63
59
 
64
- Cart.create
65
- => #<Cart id: 1, state: "open">
66
- Order.all
67
- => #<ActiveRecord::Relation [#<Cart id: 1, state: "open">]>
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
- - [ ] Bug: multiple values: Child.all query
5
- - [ ] more tests / examples
6
- - [ ] Rails 5 support
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
- - [ ] `self.abstract_class = true`
9
- - [ ] scoping…
10
- - [ ] ~~use `type` column with enum, int, string, etc.~~
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)
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Discriminable
4
- VERSION = "2.2.3"
4
+ VERSION = "3.0.0"
5
5
  end
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
- # discriminable state: { lead: "Lead" }
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 :discriminable_map, instance_writer: false
28
- class_attribute :discriminable_inverse_map, instance_writer: false
29
- class_attribute :discriminable_values, instance_writer: false
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
- # Specify the column to use for discrimination.
32
+ # This adds
33
+ # - `discriminable_attribute` and
34
+ # - `discriminalbe_value`
35
+ # class methods (plus some aliases).
33
36
  module ClassMethods
34
- def discriminable(**options)
35
- raise "Subclasses should not override .discriminable" unless base_class?
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
- # Use first key as default discriminator
43
- # { a: "C", b: "C" }.invert => { "C" => :b }
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
- def discriminable_by(attribute)
53
- raise "Subclasses should not override .discriminable_by" unless base_class?
54
-
55
- self.discriminable_map ||= discriminable_map_memoized
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
- attribute = attribute.to_s
59
- self.inheritance_column = attribute_aliases[attribute] || attribute
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.discriminable_values = values.map do |value|
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
- discriminable_inverse_map[name]
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 = discriminable_map[value])
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 = discriminable_value(attrs)
103
+ value = _discriminable_value(attrs)
89
104
  sti_class_for(value)
90
105
  end
91
106
 
92
- def discriminable_map_memoized
107
+ def _discriminable_map_memoized
93
108
  Hash.new do |map, value|
94
- map[value] = descendants.detect { |d| d.discriminable_values.include? value }&.name
109
+ map[value] = descendants.detect { |d| d._discriminable_values.include? value }&.name
95
110
  end
96
111
  end
97
112
 
98
- def discriminable_inverse_map_memoized
113
+ def _discriminable_inverse_map_memoized
99
114
  Hash.new do |map, value|
100
- map[value] = value.constantize.discriminable_values&.first
115
+ map[value] = value.constantize._discriminable_values&.first
101
116
  end
102
117
  end
103
118
 
104
- def discriminable_value(attrs)
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: 2.2.3
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-04-20 00:00:00.000000000 Z
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: Single Table Inheritencs (STI) like functionality using _any_ column,
112
- like e.g. enums, etc.
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: []