belongs_to_one_of 0.2.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 +7 -0
- data/.circleci/config.yml +53 -0
- data/.dependabot/config.yml +6 -0
- data/.gitignore +1 -0
- data/.rubocop.yml +10 -0
- data/.rubocop_todo.yml +16 -0
- data/.ruby-version +1 -0
- data/Gemfile +7 -0
- data/LICENSE.txt +22 -0
- data/README.md +178 -0
- data/belongs_to_one_of.gemspec +39 -0
- data/docs/COMPATABILITY.md +26 -0
- data/lib/belongs_to_one_of.rb +75 -0
- data/lib/belongs_to_one_of/belongs_to_one_of_model.rb +166 -0
- data/lib/belongs_to_one_of/version.rb +5 -0
- metadata +182 -0
checksums.yaml
ADDED
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
---
|
|
2
|
+
SHA256:
|
|
3
|
+
metadata.gz: 98bf15abb668273f22d84ff9bf834806e66994533eb2393ae07929f2dd675789
|
|
4
|
+
data.tar.gz: d53da88da80da202fb3cc65f041f8dbeffb2a388017dc667694452ef8182d6b8
|
|
5
|
+
SHA512:
|
|
6
|
+
metadata.gz: 8b06bcea6fa1f26f0791829451d3eedf8f82737e7710502a0222ca3bb94a97ed6402aef8fd935165bb664a45602a5c1e4a7bc529ff650bbf9d48e7e548010276
|
|
7
|
+
data.tar.gz: b1b2a1fbf7b60889cad46f9a97b61cbfb0dde782f1a11ff90b1efd189e225d2bfe94a01fbb186e99d2ce7ea31d9d2236b1b438bca23c55ff5875326ed5e35151
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
version: 2.1
|
|
2
|
+
|
|
3
|
+
jobs:
|
|
4
|
+
test:
|
|
5
|
+
parameters:
|
|
6
|
+
ruby-version:
|
|
7
|
+
type: string
|
|
8
|
+
rails-version:
|
|
9
|
+
type: string
|
|
10
|
+
|
|
11
|
+
docker:
|
|
12
|
+
- image: ruby:<<parameters.ruby-version>>
|
|
13
|
+
environment:
|
|
14
|
+
- RAILS_VERSION=<<parameters.rails-version>>
|
|
15
|
+
steps:
|
|
16
|
+
- checkout
|
|
17
|
+
|
|
18
|
+
- run: gem install bundler
|
|
19
|
+
- run: bundle install
|
|
20
|
+
|
|
21
|
+
- run: |
|
|
22
|
+
bundle exec rspec --profile 10 \
|
|
23
|
+
--format RspecJunitFormatter \
|
|
24
|
+
--out /tmp/test-results/rspec.xml \
|
|
25
|
+
--format progress \
|
|
26
|
+
spec
|
|
27
|
+
- store_test_results:
|
|
28
|
+
path: /tmp/test-results
|
|
29
|
+
|
|
30
|
+
rubocop:
|
|
31
|
+
docker:
|
|
32
|
+
- image: ruby:2.7
|
|
33
|
+
steps:
|
|
34
|
+
- checkout
|
|
35
|
+
- run: gem install bundler -v 2.1.4
|
|
36
|
+
- run: bundle install
|
|
37
|
+
- run:
|
|
38
|
+
name: Rubocop
|
|
39
|
+
command: bundle exec rubocop --parallel --extra-details --display-style-guide
|
|
40
|
+
|
|
41
|
+
workflows:
|
|
42
|
+
version: 2
|
|
43
|
+
tests:
|
|
44
|
+
jobs:
|
|
45
|
+
- test:
|
|
46
|
+
matrix:
|
|
47
|
+
parameters:
|
|
48
|
+
ruby-version: ["2.6", "2.7", "3.0"]
|
|
49
|
+
rails-version: ["5.2", "6.0", "6.1"]
|
|
50
|
+
exclude:
|
|
51
|
+
- ruby-version: "3.0"
|
|
52
|
+
rails-version: "5.2"
|
|
53
|
+
- rubocop
|
data/.gitignore
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
Gemfile.lock
|
data/.rubocop.yml
ADDED
data/.rubocop_todo.yml
ADDED
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
# This configuration was generated by
|
|
2
|
+
# `rubocop --auto-gen-config`
|
|
3
|
+
# on 2020-04-17 14:28:59 +0100 using RuboCop version 0.80.1.
|
|
4
|
+
# The point is for the user to remove these configuration records
|
|
5
|
+
# one by one as the offenses are removed from the code base.
|
|
6
|
+
# Note that changes in the inspected code, or installation of new
|
|
7
|
+
# versions of RuboCop, may require this file to be generated again.
|
|
8
|
+
|
|
9
|
+
# Offense count: 2
|
|
10
|
+
Metrics/AbcSize:
|
|
11
|
+
Max: 22
|
|
12
|
+
|
|
13
|
+
# Offense count: 3
|
|
14
|
+
# Configuration parameters: CountComments, ExcludedMethods.
|
|
15
|
+
Metrics/MethodLength:
|
|
16
|
+
Max: 37
|
data/.ruby-version
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
2.7.1
|
data/Gemfile
ADDED
data/LICENSE.txt
ADDED
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
Copyright (c) 2021 GoCardless
|
|
2
|
+
|
|
3
|
+
MIT License
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining
|
|
6
|
+
a copy of this software and associated documentation files (the
|
|
7
|
+
"Software"), to deal in the Software without restriction, including
|
|
8
|
+
without limitation the rights to use, copy, modify, merge, publish,
|
|
9
|
+
distribute, sublicense, and/or sell copies of the Software, and to
|
|
10
|
+
permit persons to whom the Software is furnished to do so, subject to
|
|
11
|
+
the following conditions:
|
|
12
|
+
|
|
13
|
+
The above copyright notice and this permission notice shall be
|
|
14
|
+
included in all copies or substantial portions of the Software.
|
|
15
|
+
|
|
16
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
|
17
|
+
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
|
|
18
|
+
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
|
|
19
|
+
NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
|
|
20
|
+
LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
|
|
21
|
+
OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
|
|
22
|
+
WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
data/README.md
ADDED
|
@@ -0,0 +1,178 @@
|
|
|
1
|
+
# belongs_to_one_of
|
|
2
|
+
Gem to support activemodel relations where one model can be a child of one of many models.
|
|
3
|
+
In our examples, we will be targeting a class `Competitor` which can either belong to a `School` or a `College`.
|
|
4
|
+
We will consider this more general concept an `organisation`.
|
|
5
|
+
|
|
6
|
+
The gem provides a simple method to declare this relationship, some validators to enforce the relationships,
|
|
7
|
+
and some helper functions to safely set and get the associated model.
|
|
8
|
+
|
|
9
|
+
## What about rails polymorphic relations?
|
|
10
|
+
|
|
11
|
+
Unlike rails polymorphic relations, this supports having a separate `id` column for each parent
|
|
12
|
+
model type (e.g. `school_id` and `college_id` instead of just `organisation_id`). This is desirable
|
|
13
|
+
(in some cases) to enable the database to use foreign keys.
|
|
14
|
+
|
|
15
|
+
The gem will also error if you try to set a resource which isn't one of the specified classes, unlike
|
|
16
|
+
a rails polymorphic relation which will accept any model class.
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
## Installation
|
|
20
|
+
|
|
21
|
+
Install the package from Rubygems:
|
|
22
|
+
```shell script
|
|
23
|
+
gem install belongs_to_one_of
|
|
24
|
+
```
|
|
25
|
+
|
|
26
|
+
Or add it to your gemfile
|
|
27
|
+
```
|
|
28
|
+
gem 'belongs_to_one_of'
|
|
29
|
+
```
|
|
30
|
+
|
|
31
|
+
For our policy on compatibility with Ruby versions, see [COMPATIBILITY.md](docs/COMPATIBILITY.md).
|
|
32
|
+
|
|
33
|
+
## Quick Start
|
|
34
|
+
|
|
35
|
+
Our code will say:
|
|
36
|
+
|
|
37
|
+
> This model belongs to an organisation, which might be a School or might be a College
|
|
38
|
+
|
|
39
|
+
This allows us to store the association in the columns `school_id` and `college_id`, which can use foreign keys,
|
|
40
|
+
but for the 99% of your code which doesn't care which is which, they can just call a method `organisation` or
|
|
41
|
+
`organisation_id` to access the resource.
|
|
42
|
+
|
|
43
|
+
The library adds a new association to `ActiveRecord` called `belongs_to_one_of :organisation, `. To use it, simply call this
|
|
44
|
+
hook in your `ActiveRecord` class:
|
|
45
|
+
|
|
46
|
+
```ruby
|
|
47
|
+
class Competitor < ActiveRecord::Base
|
|
48
|
+
belongs_to_one_of :organisation, %i[school college]
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
class School < ActiveRecord::Base
|
|
52
|
+
has_many :competitors
|
|
53
|
+
end
|
|
54
|
+
|
|
55
|
+
class College < ActiveRecord::Base
|
|
56
|
+
has_many :competitors
|
|
57
|
+
end
|
|
58
|
+
|
|
59
|
+
school = School.new
|
|
60
|
+
|
|
61
|
+
my_competitor = Competitor.create(name: 'jack', organisation: school)
|
|
62
|
+
|
|
63
|
+
my_competitor.organisation
|
|
64
|
+
# => school
|
|
65
|
+
|
|
66
|
+
my_competitor.organisation_id == school.id
|
|
67
|
+
# => true
|
|
68
|
+
```
|
|
69
|
+
|
|
70
|
+
Note that this helper calls `belongs_to :school, optional:true` and `belongs_to :college, optional:true`, so you don't have to.
|
|
71
|
+
|
|
72
|
+
## Available methods
|
|
73
|
+
|
|
74
|
+
The hook defines a few methods on your class. The names are dynamic, we will use 'organisation' as our example
|
|
75
|
+
(as per above):
|
|
76
|
+
|
|
77
|
+
### Validators
|
|
78
|
+
|
|
79
|
+
#### `belongs_to_exactly_one_[organisation]`
|
|
80
|
+
A validator that can be used to check that a model belongs to exactly one organisation
|
|
81
|
+
|
|
82
|
+
#### `belongs_to_at_most_one_[organisation]`
|
|
83
|
+
A validator that can be used to check that a model belongs to either no organisations or one organisation
|
|
84
|
+
|
|
85
|
+
#### `[organisation_type]_matches_[organisation]`
|
|
86
|
+
A validator that can be used to check that the type of model matches the model. Only relevant when
|
|
87
|
+
`include_type_column` is true
|
|
88
|
+
|
|
89
|
+
### Getters & Setters
|
|
90
|
+
|
|
91
|
+
#### `[organisation]=`
|
|
92
|
+
Allows you to create a new instance of the model with the interface:
|
|
93
|
+
|
|
94
|
+
```ruby
|
|
95
|
+
Competitor.new(
|
|
96
|
+
organisation: my_school,
|
|
97
|
+
name: "Joe Bloggs"
|
|
98
|
+
)
|
|
99
|
+
```
|
|
100
|
+
|
|
101
|
+
This will raise a `ModelNotFound` exception if the organisation is not one of the permitted model types
|
|
102
|
+
|
|
103
|
+
#### `[organisation]`
|
|
104
|
+
Allows you to get the linked resource via `.organisation` e.g.:
|
|
105
|
+
```ruby
|
|
106
|
+
my_competitor.organisation
|
|
107
|
+
```
|
|
108
|
+
#### `[organisation]_id`
|
|
109
|
+
Allows you to get the linked resource's id via `.organisation_id` e.g.:
|
|
110
|
+
```ruby
|
|
111
|
+
my_competitor.organisation_id
|
|
112
|
+
```
|
|
113
|
+
|
|
114
|
+
#### `[organisation]_type`
|
|
115
|
+
This is only set when the associations are configured with `include_type_column` (see below). This allows you to access the resource type via
|
|
116
|
+
`.organisation_type` e.g.:
|
|
117
|
+
```ruby
|
|
118
|
+
my_competitor.organisation_type
|
|
119
|
+
# => 'School'
|
|
120
|
+
```
|
|
121
|
+
|
|
122
|
+
## Configuration Options
|
|
123
|
+
|
|
124
|
+
### `include_type_column`
|
|
125
|
+
|
|
126
|
+
By default, the library assumes that the underlying table looks like:
|
|
127
|
+
|
|
128
|
+
`id` | `name` | `school_id` | `college_id`
|
|
129
|
+
----|----|---|---
|
|
130
|
+
1 | Aaron J Aaronson | | COL123
|
|
131
|
+
2 | Betty F Parker | SCH456 |
|
|
132
|
+
|
|
133
|
+
however you can set `include_type_column: true` to explicitly store what type of model is connected, e.g.:
|
|
134
|
+
|
|
135
|
+
```ruby
|
|
136
|
+
belongs_to_one_of :organisation, %i[school college], include_type_column: true
|
|
137
|
+
```
|
|
138
|
+
|
|
139
|
+
`id` | `name` | `organisation_type` | `school_id` | `college_id`
|
|
140
|
+
----|----|---|---|---
|
|
141
|
+
1 | Aaron J Aaronson | College | | COL123
|
|
142
|
+
2 | Betty F Parker | School | SCH456 |
|
|
143
|
+
|
|
144
|
+
if the column is not called `[organisation]_type`, you can specify the column name e.g.
|
|
145
|
+
|
|
146
|
+
```ruby
|
|
147
|
+
belongs_to_one_of :organisation, %i[school college], include_type_column: :type_of_organisation
|
|
148
|
+
```
|
|
149
|
+
|
|
150
|
+
`id` | `name` | `type_of_organisation` | `school_id` | `college_id`
|
|
151
|
+
----|----|---|---|---
|
|
152
|
+
1 | Aaron J Aaronson | College | | COL123
|
|
153
|
+
2 | Betty F Parker | School | SCH456 |
|
|
154
|
+
|
|
155
|
+
|
|
156
|
+
### `type_column_value`
|
|
157
|
+
|
|
158
|
+
If you have `include_type_column: true` set, by default we assume you want to store the classname in the db.
|
|
159
|
+
However, there may be some logic that you want to apply. If you pass a Proc to `type_column_value`
|
|
160
|
+
you can add your own logic to determine what goes into the db.
|
|
161
|
+
|
|
162
|
+
```ruby
|
|
163
|
+
belongs_to_one_of :organisation, %i[school college], include_type_column: true,
|
|
164
|
+
type_column_value: ->(resource) { resource.class.downcase }
|
|
165
|
+
```
|
|
166
|
+
|
|
167
|
+
`id` | `name` | `organisation_type` | `school_id` | `college_id`
|
|
168
|
+
----|----|---|---|---
|
|
169
|
+
1 | Aaron J Aaronson | college | | COL123
|
|
170
|
+
2 | Betty F Parker | school | SCH456 |
|
|
171
|
+
|
|
172
|
+
|
|
173
|
+
## License & Contributing
|
|
174
|
+
|
|
175
|
+
* BelongsToOneOf is available as open source under the terms of the [MIT License](http://opensource.org/licenses/MIT).
|
|
176
|
+
* Bug reports and pull requests are welcome on GitHub at https://github.com/gocardless/belongs-to-one-of.
|
|
177
|
+
|
|
178
|
+
GoCardless ♥ open source. If you do too, come [join us](https://gocardless.com/about/careers/).
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
lib = File.expand_path("lib", __dir__)
|
|
4
|
+
$LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
|
|
5
|
+
|
|
6
|
+
require "belongs_to_one_of/version"
|
|
7
|
+
|
|
8
|
+
Gem::Specification.new do |spec|
|
|
9
|
+
spec.name = "belongs_to_one_of"
|
|
10
|
+
spec.version = BelongsToOneOf::VERSION
|
|
11
|
+
spec.authors = ["GoCardless Engineering"]
|
|
12
|
+
spec.email = ["engineering@gocardless.com"]
|
|
13
|
+
|
|
14
|
+
spec.summary = "A small library that helps with models which can have " \
|
|
15
|
+
"multiple parent model types"
|
|
16
|
+
spec.homepage = "https://github.com/gocardless/belongs-to-one-of"
|
|
17
|
+
spec.license = "MIT"
|
|
18
|
+
|
|
19
|
+
# Specify which files should be added to the gem when it is released.
|
|
20
|
+
# The `git ls-files -z` loads the files in the RubyGem that have been added into git.
|
|
21
|
+
spec.files = Dir.chdir(File.expand_path(__dir__)) do
|
|
22
|
+
`git ls-files -z`.split("\x0").reject { |f| f.match(%r{^(test|spec|features)/}) }
|
|
23
|
+
end
|
|
24
|
+
spec.require_paths = ["lib"]
|
|
25
|
+
|
|
26
|
+
spec.required_ruby_version = ">= 2.6"
|
|
27
|
+
|
|
28
|
+
spec.add_development_dependency "bundler"
|
|
29
|
+
spec.add_development_dependency "gc_ruboconfig", "~> 2.25.0"
|
|
30
|
+
spec.add_development_dependency "pry-byebug"
|
|
31
|
+
spec.add_development_dependency "rspec", "~> 3.9"
|
|
32
|
+
spec.add_development_dependency "rspec_junit_formatter", "~> 0.4"
|
|
33
|
+
|
|
34
|
+
# For integration testing
|
|
35
|
+
spec.add_development_dependency "sqlite3", "~> 1.4.1"
|
|
36
|
+
|
|
37
|
+
spec.add_dependency "activerecord", ">= 5.2", "< 6.2"
|
|
38
|
+
spec.add_dependency "activesupport", ">= 5.2", "< 6.2"
|
|
39
|
+
end
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
# Compatibility
|
|
2
|
+
|
|
3
|
+
Our goal as maintainers is for the library to be compatible with all supported
|
|
4
|
+
versions of Ruby.
|
|
5
|
+
|
|
6
|
+
Specifically, any CRuby/MRI version that has not received an End of Life notice
|
|
7
|
+
([e.g. this notice for Ruby 2.1](https://www.ruby-lang.org/en/news/2017/04/01/support-of-ruby-2-1-has-ended/))
|
|
8
|
+
is supported.
|
|
9
|
+
|
|
10
|
+
To that end, [our build matrix](../.circleci/config.yml) includes all these versions.
|
|
11
|
+
|
|
12
|
+
Any time BelongsToOneOf doesn't work on a supported version of Ruby, it's a bug, and can be
|
|
13
|
+
reported [here](https://github.com/gocardless/belongs-to-one-of/issues).
|
|
14
|
+
|
|
15
|
+
# Deprecation
|
|
16
|
+
|
|
17
|
+
Whenever a version of Ruby falls out of support, we will mirror that change in BelongsToOneOf
|
|
18
|
+
by updating the build matrix and releasing a new major version.
|
|
19
|
+
|
|
20
|
+
At that point, we will close any issues that only affect the unsupported version, and may
|
|
21
|
+
choose to remove any workarounds from the code that are only necessary for the unsupported
|
|
22
|
+
version.
|
|
23
|
+
|
|
24
|
+
We will then bump the major version of BelongsToOneOf, to indicate the break in compatibility.
|
|
25
|
+
Even if the new version of BelongsToOneOf happens to work on the unsupported version of Ruby, we
|
|
26
|
+
consider compatibility to be broken at this point.
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "active_record"
|
|
4
|
+
require "active_record/associations"
|
|
5
|
+
require_relative "belongs_to_one_of/belongs_to_one_of_model"
|
|
6
|
+
|
|
7
|
+
# We assume that the connected model is called a 'resource',
|
|
8
|
+
# for the purposes of naming variables in this file
|
|
9
|
+
|
|
10
|
+
module ActiveRecord
|
|
11
|
+
module Associations
|
|
12
|
+
module ClassMethods
|
|
13
|
+
include BelongsToOneOf
|
|
14
|
+
|
|
15
|
+
def belongs_to_one_of(
|
|
16
|
+
resource_key,
|
|
17
|
+
raw_possible_resource_types,
|
|
18
|
+
include_type_column: false
|
|
19
|
+
)
|
|
20
|
+
resource_type_field = "#{resource_key}_type"
|
|
21
|
+
unless [true, false].include?(include_type_column)
|
|
22
|
+
resource_type_field =
|
|
23
|
+
include_type_column
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
config_model = BelongsToOneOfModel.new(
|
|
27
|
+
resource_key,
|
|
28
|
+
raw_possible_resource_types,
|
|
29
|
+
include_type_column,
|
|
30
|
+
resource_type_field,
|
|
31
|
+
self,
|
|
32
|
+
)
|
|
33
|
+
|
|
34
|
+
# validators
|
|
35
|
+
define_method "belongs_to_exactly_one_#{resource_key}" do
|
|
36
|
+
config_model.validate_exactly_one_resource(self)
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
define_method "belongs_to_at_most_one_#{resource_key}" do
|
|
40
|
+
config_model.validate_at_most_one_resource(self)
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
if include_type_column
|
|
44
|
+
define_method "#{resource_type_field}_matches_#{resource_key}" do
|
|
45
|
+
config_model.validate_correct_resource_type(self)
|
|
46
|
+
end
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
# setters
|
|
50
|
+
define_method "#{resource_key}=" do |resource|
|
|
51
|
+
config_model.resource_setter(resource, self)
|
|
52
|
+
end
|
|
53
|
+
|
|
54
|
+
# getters
|
|
55
|
+
define_method resource_key do
|
|
56
|
+
config_model.resource_getter(self)
|
|
57
|
+
end
|
|
58
|
+
|
|
59
|
+
define_method "#{resource_key}_id" do
|
|
60
|
+
config_model.resource_id_getter(self)
|
|
61
|
+
end
|
|
62
|
+
|
|
63
|
+
unless include_type_column
|
|
64
|
+
define_method "#{resource_key}_type" do
|
|
65
|
+
config_model.resource_type_getter(self)
|
|
66
|
+
end
|
|
67
|
+
end
|
|
68
|
+
end
|
|
69
|
+
|
|
70
|
+
def self.included(base)
|
|
71
|
+
base.extend(ClassMethods)
|
|
72
|
+
end
|
|
73
|
+
end
|
|
74
|
+
end
|
|
75
|
+
end
|
|
@@ -0,0 +1,166 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module BelongsToOneOf
|
|
4
|
+
class BelongsToOneOfModel
|
|
5
|
+
class InvalidParamsException < ArgumentError; end
|
|
6
|
+
|
|
7
|
+
def initialize(
|
|
8
|
+
resource_key,
|
|
9
|
+
raw_possible_resource_types,
|
|
10
|
+
include_type_column,
|
|
11
|
+
resource_type_field,
|
|
12
|
+
model_class
|
|
13
|
+
)
|
|
14
|
+
@resource_key = resource_key
|
|
15
|
+
@possible_resource_types = normalise_resource_types(raw_possible_resource_types)
|
|
16
|
+
@include_type_column = include_type_column
|
|
17
|
+
@resource_type_field = resource_type_field
|
|
18
|
+
@model_class = model_class
|
|
19
|
+
validate_config
|
|
20
|
+
initialize_associations
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
attr_reader :resource_key, :possible_resource_types, :include_type_column,
|
|
24
|
+
:resource_type_field, :model_class
|
|
25
|
+
|
|
26
|
+
# TODO: I18n the error messages (provide translations??)
|
|
27
|
+
def validate_exactly_one_resource(model)
|
|
28
|
+
return if number_of_resources(model) == 1
|
|
29
|
+
|
|
30
|
+
model.errors.add(:base, "model must belong to exactly one #{resource_key}")
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
def validate_at_most_one_resource(model)
|
|
34
|
+
return unless number_of_resources(model) > 1
|
|
35
|
+
|
|
36
|
+
model.errors.add(:base, "model must belong to at most one #{resource_key}")
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
def validate_correct_resource_type(model)
|
|
40
|
+
resource = model.send(resource_key)
|
|
41
|
+
return unless resource
|
|
42
|
+
|
|
43
|
+
actual_resource_type = resource_type_getter(model)
|
|
44
|
+
specified_resource_type = model.send(resource_type_field)
|
|
45
|
+
|
|
46
|
+
unless actual_resource_type == specified_resource_type
|
|
47
|
+
model.errors.add(
|
|
48
|
+
resource_type_field,
|
|
49
|
+
"#{resource_type_field} must match the type of #{resource_key}",
|
|
50
|
+
)
|
|
51
|
+
end
|
|
52
|
+
end
|
|
53
|
+
|
|
54
|
+
def resource_setter(resource, model)
|
|
55
|
+
return if resource.nil?
|
|
56
|
+
|
|
57
|
+
possible_resource_types.each_key do |resource_type|
|
|
58
|
+
model.public_send("#{resource_type}=", nil)
|
|
59
|
+
end
|
|
60
|
+
|
|
61
|
+
model.instance_variable_set("@#{resource_key}", resource)
|
|
62
|
+
resource_type_accessor = find_resource_accessor(resource, model)
|
|
63
|
+
|
|
64
|
+
unless resource_type_accessor
|
|
65
|
+
message = "one of #{possible_resource_types.keys.join(', ')} expected, "\
|
|
66
|
+
"got #{resource.inspect} which is an instance of "\
|
|
67
|
+
"#{resource.class}(##{resource.class.object_id})"
|
|
68
|
+
raise ActiveRecord::AssociationTypeMismatch, message
|
|
69
|
+
end
|
|
70
|
+
|
|
71
|
+
resource_setter_method = "#{resource_type_accessor}="
|
|
72
|
+
model.public_send(resource_setter_method, resource)
|
|
73
|
+
|
|
74
|
+
if include_type_column
|
|
75
|
+
resource_type_setter_method = "#{resource_type_field}="
|
|
76
|
+
resource_type_to_store = find_resource_type(resource, model)
|
|
77
|
+
model.public_send(resource_type_setter_method, resource_type_to_store)
|
|
78
|
+
end
|
|
79
|
+
end
|
|
80
|
+
|
|
81
|
+
def resource_getter(model)
|
|
82
|
+
possible_resource_types.keys.detect do |resource_type|
|
|
83
|
+
return model.send(resource_type) if model.send(resource_type)
|
|
84
|
+
end
|
|
85
|
+
end
|
|
86
|
+
|
|
87
|
+
def resource_id_getter(model)
|
|
88
|
+
possible_resource_types.values.detect do |id_column|
|
|
89
|
+
return model.send(id_column) if model.send(id_column)
|
|
90
|
+
end
|
|
91
|
+
end
|
|
92
|
+
|
|
93
|
+
def resource_type_getter(model)
|
|
94
|
+
resource = model.send(resource_key)
|
|
95
|
+
return unless resource
|
|
96
|
+
|
|
97
|
+
possible_resource_types.keys.detect do |resource_type_accessor|
|
|
98
|
+
reflection = model.class.reflect_on_association(resource_type_accessor)
|
|
99
|
+
return reflection.class_name if resource.is_a?(reflection.klass)
|
|
100
|
+
end
|
|
101
|
+
end
|
|
102
|
+
|
|
103
|
+
private
|
|
104
|
+
|
|
105
|
+
def validate_config
|
|
106
|
+
unless resource_key.is_a?(Symbol)
|
|
107
|
+
raise InvalidParamsException,
|
|
108
|
+
"expected resource_key to be a symbol, received #{resource_key.class}"
|
|
109
|
+
end
|
|
110
|
+
end
|
|
111
|
+
|
|
112
|
+
def initialize_associations
|
|
113
|
+
possible_resource_types.each do |resource_type_accessor, resource_id_column|
|
|
114
|
+
opts = { optional: true }
|
|
115
|
+
|
|
116
|
+
# Only set :foreign_key for a complex relation. Simple relations can be safely
|
|
117
|
+
# "inflected" (i.e. Rails knows these objects are the same...)
|
|
118
|
+
# foo = Foo.find(1)
|
|
119
|
+
# foo.bar.foo == foo
|
|
120
|
+
if resource_id_column.to_s.gsub!(/_id$/, "") != resource_type_accessor.to_s
|
|
121
|
+
opts[:foreign_key] = resource_id_column
|
|
122
|
+
end
|
|
123
|
+
|
|
124
|
+
model_class.belongs_to(resource_type_accessor, **opts)
|
|
125
|
+
end
|
|
126
|
+
end
|
|
127
|
+
|
|
128
|
+
def normalise_resource_types(raw_possible_resource_types)
|
|
129
|
+
if raw_possible_resource_types.is_a?(Array)
|
|
130
|
+
raw_possible_resource_types.each_with_object({}) do |classname, hash|
|
|
131
|
+
unless classname.is_a?(Symbol)
|
|
132
|
+
raise InvalidParamsException, "expected a symbol, received #{classname}"
|
|
133
|
+
end
|
|
134
|
+
|
|
135
|
+
hash[classname] = "#{classname.to_s.underscore}_id".to_sym
|
|
136
|
+
end
|
|
137
|
+
elsif raw_possible_resource_types.is_a?(Hash)
|
|
138
|
+
raw_possible_resource_types
|
|
139
|
+
else
|
|
140
|
+
raise InvalidParamsException,
|
|
141
|
+
"possible_resource_types must be an Array or a Hash, " \
|
|
142
|
+
"received #{raw_possible_resource_types.class}"
|
|
143
|
+
end
|
|
144
|
+
end
|
|
145
|
+
|
|
146
|
+
def number_of_resources(model)
|
|
147
|
+
possible_resource_types.keys.count do |resource_type_accessor|
|
|
148
|
+
model.send(resource_type_accessor)
|
|
149
|
+
end
|
|
150
|
+
end
|
|
151
|
+
|
|
152
|
+
def find_resource_accessor(resource, model)
|
|
153
|
+
possible_resource_types.keys.detect do |resource_type_accessor|
|
|
154
|
+
reflection = model.class.reflect_on_association(resource_type_accessor)
|
|
155
|
+
return resource_type_accessor if resource.is_a?(reflection.klass)
|
|
156
|
+
end
|
|
157
|
+
end
|
|
158
|
+
|
|
159
|
+
def find_resource_type(resource, model)
|
|
160
|
+
possible_resource_types.keys.detect do |resource_type_accessor|
|
|
161
|
+
reflection = model.class.reflect_on_association(resource_type_accessor)
|
|
162
|
+
return reflection.class_name if resource.is_a?(reflection.klass)
|
|
163
|
+
end
|
|
164
|
+
end
|
|
165
|
+
end
|
|
166
|
+
end
|
metadata
ADDED
|
@@ -0,0 +1,182 @@
|
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
|
2
|
+
name: belongs_to_one_of
|
|
3
|
+
version: !ruby/object:Gem::Version
|
|
4
|
+
version: 0.2.0
|
|
5
|
+
platform: ruby
|
|
6
|
+
authors:
|
|
7
|
+
- GoCardless Engineering
|
|
8
|
+
autorequire:
|
|
9
|
+
bindir: bin
|
|
10
|
+
cert_chain: []
|
|
11
|
+
date: 2021-06-11 00:00:00.000000000 Z
|
|
12
|
+
dependencies:
|
|
13
|
+
- !ruby/object:Gem::Dependency
|
|
14
|
+
name: bundler
|
|
15
|
+
requirement: !ruby/object:Gem::Requirement
|
|
16
|
+
requirements:
|
|
17
|
+
- - ">="
|
|
18
|
+
- !ruby/object:Gem::Version
|
|
19
|
+
version: '0'
|
|
20
|
+
type: :development
|
|
21
|
+
prerelease: false
|
|
22
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
23
|
+
requirements:
|
|
24
|
+
- - ">="
|
|
25
|
+
- !ruby/object:Gem::Version
|
|
26
|
+
version: '0'
|
|
27
|
+
- !ruby/object:Gem::Dependency
|
|
28
|
+
name: gc_ruboconfig
|
|
29
|
+
requirement: !ruby/object:Gem::Requirement
|
|
30
|
+
requirements:
|
|
31
|
+
- - "~>"
|
|
32
|
+
- !ruby/object:Gem::Version
|
|
33
|
+
version: 2.25.0
|
|
34
|
+
type: :development
|
|
35
|
+
prerelease: false
|
|
36
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
37
|
+
requirements:
|
|
38
|
+
- - "~>"
|
|
39
|
+
- !ruby/object:Gem::Version
|
|
40
|
+
version: 2.25.0
|
|
41
|
+
- !ruby/object:Gem::Dependency
|
|
42
|
+
name: pry-byebug
|
|
43
|
+
requirement: !ruby/object:Gem::Requirement
|
|
44
|
+
requirements:
|
|
45
|
+
- - ">="
|
|
46
|
+
- !ruby/object:Gem::Version
|
|
47
|
+
version: '0'
|
|
48
|
+
type: :development
|
|
49
|
+
prerelease: false
|
|
50
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
51
|
+
requirements:
|
|
52
|
+
- - ">="
|
|
53
|
+
- !ruby/object:Gem::Version
|
|
54
|
+
version: '0'
|
|
55
|
+
- !ruby/object:Gem::Dependency
|
|
56
|
+
name: rspec
|
|
57
|
+
requirement: !ruby/object:Gem::Requirement
|
|
58
|
+
requirements:
|
|
59
|
+
- - "~>"
|
|
60
|
+
- !ruby/object:Gem::Version
|
|
61
|
+
version: '3.9'
|
|
62
|
+
type: :development
|
|
63
|
+
prerelease: false
|
|
64
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
65
|
+
requirements:
|
|
66
|
+
- - "~>"
|
|
67
|
+
- !ruby/object:Gem::Version
|
|
68
|
+
version: '3.9'
|
|
69
|
+
- !ruby/object:Gem::Dependency
|
|
70
|
+
name: rspec_junit_formatter
|
|
71
|
+
requirement: !ruby/object:Gem::Requirement
|
|
72
|
+
requirements:
|
|
73
|
+
- - "~>"
|
|
74
|
+
- !ruby/object:Gem::Version
|
|
75
|
+
version: '0.4'
|
|
76
|
+
type: :development
|
|
77
|
+
prerelease: false
|
|
78
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
79
|
+
requirements:
|
|
80
|
+
- - "~>"
|
|
81
|
+
- !ruby/object:Gem::Version
|
|
82
|
+
version: '0.4'
|
|
83
|
+
- !ruby/object:Gem::Dependency
|
|
84
|
+
name: sqlite3
|
|
85
|
+
requirement: !ruby/object:Gem::Requirement
|
|
86
|
+
requirements:
|
|
87
|
+
- - "~>"
|
|
88
|
+
- !ruby/object:Gem::Version
|
|
89
|
+
version: 1.4.1
|
|
90
|
+
type: :development
|
|
91
|
+
prerelease: false
|
|
92
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
93
|
+
requirements:
|
|
94
|
+
- - "~>"
|
|
95
|
+
- !ruby/object:Gem::Version
|
|
96
|
+
version: 1.4.1
|
|
97
|
+
- !ruby/object:Gem::Dependency
|
|
98
|
+
name: activerecord
|
|
99
|
+
requirement: !ruby/object:Gem::Requirement
|
|
100
|
+
requirements:
|
|
101
|
+
- - ">="
|
|
102
|
+
- !ruby/object:Gem::Version
|
|
103
|
+
version: '5.2'
|
|
104
|
+
- - "<"
|
|
105
|
+
- !ruby/object:Gem::Version
|
|
106
|
+
version: '6.2'
|
|
107
|
+
type: :runtime
|
|
108
|
+
prerelease: false
|
|
109
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
110
|
+
requirements:
|
|
111
|
+
- - ">="
|
|
112
|
+
- !ruby/object:Gem::Version
|
|
113
|
+
version: '5.2'
|
|
114
|
+
- - "<"
|
|
115
|
+
- !ruby/object:Gem::Version
|
|
116
|
+
version: '6.2'
|
|
117
|
+
- !ruby/object:Gem::Dependency
|
|
118
|
+
name: activesupport
|
|
119
|
+
requirement: !ruby/object:Gem::Requirement
|
|
120
|
+
requirements:
|
|
121
|
+
- - ">="
|
|
122
|
+
- !ruby/object:Gem::Version
|
|
123
|
+
version: '5.2'
|
|
124
|
+
- - "<"
|
|
125
|
+
- !ruby/object:Gem::Version
|
|
126
|
+
version: '6.2'
|
|
127
|
+
type: :runtime
|
|
128
|
+
prerelease: false
|
|
129
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
130
|
+
requirements:
|
|
131
|
+
- - ">="
|
|
132
|
+
- !ruby/object:Gem::Version
|
|
133
|
+
version: '5.2'
|
|
134
|
+
- - "<"
|
|
135
|
+
- !ruby/object:Gem::Version
|
|
136
|
+
version: '6.2'
|
|
137
|
+
description:
|
|
138
|
+
email:
|
|
139
|
+
- engineering@gocardless.com
|
|
140
|
+
executables: []
|
|
141
|
+
extensions: []
|
|
142
|
+
extra_rdoc_files: []
|
|
143
|
+
files:
|
|
144
|
+
- ".circleci/config.yml"
|
|
145
|
+
- ".dependabot/config.yml"
|
|
146
|
+
- ".gitignore"
|
|
147
|
+
- ".rubocop.yml"
|
|
148
|
+
- ".rubocop_todo.yml"
|
|
149
|
+
- ".ruby-version"
|
|
150
|
+
- Gemfile
|
|
151
|
+
- LICENSE.txt
|
|
152
|
+
- README.md
|
|
153
|
+
- belongs_to_one_of.gemspec
|
|
154
|
+
- docs/COMPATABILITY.md
|
|
155
|
+
- lib/belongs_to_one_of.rb
|
|
156
|
+
- lib/belongs_to_one_of/belongs_to_one_of_model.rb
|
|
157
|
+
- lib/belongs_to_one_of/version.rb
|
|
158
|
+
homepage: https://github.com/gocardless/belongs-to-one-of
|
|
159
|
+
licenses:
|
|
160
|
+
- MIT
|
|
161
|
+
metadata: {}
|
|
162
|
+
post_install_message:
|
|
163
|
+
rdoc_options: []
|
|
164
|
+
require_paths:
|
|
165
|
+
- lib
|
|
166
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
|
167
|
+
requirements:
|
|
168
|
+
- - ">="
|
|
169
|
+
- !ruby/object:Gem::Version
|
|
170
|
+
version: '2.6'
|
|
171
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
|
172
|
+
requirements:
|
|
173
|
+
- - ">="
|
|
174
|
+
- !ruby/object:Gem::Version
|
|
175
|
+
version: '0'
|
|
176
|
+
requirements: []
|
|
177
|
+
rubygems_version: 3.1.3
|
|
178
|
+
signing_key:
|
|
179
|
+
specification_version: 4
|
|
180
|
+
summary: A small library that helps with models which can have multiple parent model
|
|
181
|
+
types
|
|
182
|
+
test_files: []
|