activerecord-exclusive-arc 0.1.0 → 0.2.2
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/README.md +15 -5
- data/lib/exclusive_arc/model.rb +30 -17
- data/lib/exclusive_arc/version.rb +1 -1
- data/lib/generators/exclusive_arc_generator.rb +55 -8
- data/lib/generators/templates/migration.rb.erb +2 -6
- metadata +2 -22
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: b3824d9259213c59c55ee5287cc9bd3b8e76924be405a269c6752434811c2599
|
4
|
+
data.tar.gz: 2e8b6ad42b5b4cad774e7ef56e21c54fa9a0e8223fce05762ecb60cf791b4b87
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 28d0736dfd28b87e367ab62d9ac1c39cc556bc76d999b390a4b6220b88ac566bcac66c6fda36b0237a5442180b44602c72e6ef837ed99a4f281b0e42da4aac81
|
7
|
+
data.tar.gz: e62da6627153c7853dde9bba360ec654ea28baf088bb4db6261fc77dd88fc78d1bf4b5f5d2c2d777682d0233e7f6ab72567ed1790e8813670b4964ec69a063a8
|
data/README.md
CHANGED
@@ -10,7 +10,7 @@ fact that the Ruby class name is stored in the database as a string. If you want
|
|
10
10
|
Ruby class used for such reasons, you must also update the database strings that represent it. The bleeding
|
11
11
|
of application-layer definitions into the database may become a liability.
|
12
12
|
|
13
|
-
Another common argument concerns referential integrity.
|
13
|
+
Another common argument concerns referential integrity. _Foreign Key Constraints_ are a common mechanism to
|
14
14
|
ensure primary keys of tables can be reliably used as foreign keys on others. This becomes harder to enforce
|
15
15
|
when a column that represents a Ruby class is one of the components required for unique identification.
|
16
16
|
|
@@ -19,8 +19,8 @@ polymorphic: true` relationship and the fact that polymorphic indexes require mu
|
|
19
19
|
|
20
20
|
### So how does this work?
|
21
21
|
|
22
|
-
It reduces the boilerplate of managing a
|
23
|
-
|
22
|
+
It reduces the boilerplate of managing a _Polymorphic Assication_ modeled as a pattern called an _Exclusive
|
23
|
+
Arc_. This maps nicely to a database constraint, a set of optional `belongs_to` relationships, some
|
24
24
|
polymorphic methods, and an `ActiveRecord` validation for good measure.
|
25
25
|
|
26
26
|
## How to use
|
@@ -49,7 +49,7 @@ This will inject code into your `Comment` Model:
|
|
49
49
|
```ruby
|
50
50
|
class Comment < ApplicationRecord
|
51
51
|
include ExclusiveArc::Model
|
52
|
-
|
52
|
+
has_exclusive_arc :commentable, [:post, :comment]
|
53
53
|
end
|
54
54
|
```
|
55
55
|
|
@@ -73,6 +73,17 @@ end
|
|
73
73
|
|
74
74
|
It's a bit more involved than that, but it demonstrates the essense of the API as an `ActiveRecord` user.
|
75
75
|
|
76
|
+
If you need to customize a specific `belongs_to` relationship, you can do so by declaring it before
|
77
|
+
`has_exclusive_arc`:
|
78
|
+
|
79
|
+
```ruby
|
80
|
+
class Comment < ApplicationRecord
|
81
|
+
include ExclusiveArc::Model
|
82
|
+
belongs_to :post, -> { where(comments_enabled: true) }, optional: true
|
83
|
+
has_exclusive_arc :commentable, [:post, :comment]
|
84
|
+
end
|
85
|
+
```
|
86
|
+
|
76
87
|
Continuing with our example, the generator command would also produce a migration that looks like this:
|
77
88
|
|
78
89
|
```ruby
|
@@ -135,4 +146,3 @@ Bug reports and pull requests are welcome on GitHub at https://github.com/waymon
|
|
135
146
|
### License
|
136
147
|
|
137
148
|
The gem is available as open source under the terms of the [MIT License](https://opensource.org/licenses/MIT).
|
138
|
-
|
data/lib/exclusive_arc/model.rb
CHANGED
@@ -8,27 +8,39 @@ module ExclusiveArc
|
|
8
8
|
end
|
9
9
|
|
10
10
|
class_methods do
|
11
|
-
def
|
12
|
-
|
13
|
-
|
14
|
-
|
15
|
-
|
16
|
-
exclusive_arcs[name] = Definition.new(
|
17
|
-
reflections: reflections.slice(*belong_tos.map(&:to_s)),
|
18
|
-
options: options
|
19
|
-
)
|
11
|
+
def has_exclusive_arc(*args)
|
12
|
+
arc = args[0]
|
13
|
+
belong_tos = args[1]
|
14
|
+
belong_tos.map do |option|
|
15
|
+
next if reflections[option.to_s]
|
20
16
|
|
21
|
-
|
22
|
-
|
23
|
-
|
24
|
-
|
17
|
+
belongs_to(option, optional: true)
|
18
|
+
end
|
19
|
+
exclusive_arcs[arc] = Definition.new(
|
20
|
+
reflections: reflections.slice(*belong_tos.map(&:to_s)),
|
21
|
+
options: args[2] || {}
|
22
|
+
)
|
25
23
|
|
26
|
-
|
27
|
-
|
24
|
+
belong_tos.each do |option|
|
25
|
+
class_eval <<-RUBY, __FILE__, __LINE__ + 1
|
26
|
+
def #{option}=(polymorphic)
|
27
|
+
@#{arc} = nil unless @#{arc} == polymorphic
|
28
|
+
super
|
28
29
|
end
|
29
30
|
RUBY
|
30
31
|
end
|
31
32
|
|
33
|
+
class_eval <<-RUBY, __FILE__, __LINE__ + 1
|
34
|
+
def #{arc}
|
35
|
+
@#{arc} ||= (#{belong_tos.join(" || ")})
|
36
|
+
end
|
37
|
+
|
38
|
+
def #{arc}=(polymorphic)
|
39
|
+
assign_exclusive_arc(:#{arc}, polymorphic)
|
40
|
+
@#{arc} = polymorphic
|
41
|
+
end
|
42
|
+
RUBY
|
43
|
+
|
32
44
|
validate :validate_exclusive_arcs
|
33
45
|
end
|
34
46
|
end
|
@@ -36,9 +48,10 @@ module ExclusiveArc
|
|
36
48
|
private
|
37
49
|
|
38
50
|
def assign_exclusive_arc(arc, polymorphic)
|
39
|
-
exclusive_arcs.fetch(arc).reflections.
|
40
|
-
|
51
|
+
attributes = exclusive_arcs.fetch(arc).reflections.to_h do |name, reflection|
|
52
|
+
[name, polymorphic.is_a?(reflection.klass) ? polymorphic : nil]
|
41
53
|
end
|
54
|
+
assign_attributes attributes
|
42
55
|
end
|
43
56
|
|
44
57
|
def validate_exclusive_arcs
|
@@ -27,12 +27,18 @@ class ExclusiveArcGenerator < ActiveRecord::Generators::Base
|
|
27
27
|
|
28
28
|
def inject_exclusive_arc_into_model
|
29
29
|
indents = " " * (class_name.scan("::").count + 1)
|
30
|
+
inject_into_class(
|
31
|
+
model_file_path,
|
32
|
+
class_name.demodulize,
|
33
|
+
<<~RB
|
34
|
+
#{indents}has_exclusive_arc #{model_exclusive_arcs}
|
35
|
+
RB
|
36
|
+
)
|
30
37
|
inject_into_class(
|
31
38
|
model_file_path,
|
32
39
|
class_name.demodulize,
|
33
40
|
<<~RB
|
34
41
|
#{indents}include ExclusiveArc::Model
|
35
|
-
#{indents}exclusive_arc #{model_exclusive_arcs}
|
36
42
|
RB
|
37
43
|
)
|
38
44
|
end
|
@@ -44,12 +50,30 @@ class ExclusiveArcGenerator < ActiveRecord::Generators::Base
|
|
44
50
|
string
|
45
51
|
end
|
46
52
|
|
53
|
+
def add_references
|
54
|
+
belong_tos.map do |reference|
|
55
|
+
add_reference(reference)
|
56
|
+
end.join("\n")
|
57
|
+
end
|
58
|
+
|
47
59
|
def add_reference(reference)
|
48
|
-
|
49
|
-
type = reference_type(reference)
|
50
|
-
|
51
|
-
|
52
|
-
|
60
|
+
foreign_key = foreign_key_name(reference)
|
61
|
+
type = reference_type(reference).downcase
|
62
|
+
if foreign_key == "#{reference}_id"
|
63
|
+
string = " add_reference :#{table_name}, :#{reference}"
|
64
|
+
string += ", type: :#{type}" unless /int/.match?(type)
|
65
|
+
string += ", foreign_key: true" unless options[:skip_foreign_key_constraints]
|
66
|
+
string += ", index: {where: \"#{foreign_key} IS NOT NULL\"}" unless options[:skip_foreign_key_indexes]
|
67
|
+
else
|
68
|
+
string = " add_column :#{table_name}, :#{foreign_key}, :#{type}"
|
69
|
+
unless options[:skip_foreign_key_constraints]
|
70
|
+
referenced_table_name = reference_table_name(reference)
|
71
|
+
string += "\n add_foreign_key :#{table_name}, :#{referenced_table_name}, column: :#{foreign_key}"
|
72
|
+
end
|
73
|
+
unless options[:skip_foreign_key_indexes]
|
74
|
+
string += "\n add_index :#{table_name}, :#{foreign_key}, where: \"#{foreign_key} IS NOT NULL\""
|
75
|
+
end
|
76
|
+
end
|
53
77
|
string
|
54
78
|
end
|
55
79
|
|
@@ -62,15 +86,38 @@ class ExclusiveArcGenerator < ActiveRecord::Generators::Base
|
|
62
86
|
end
|
63
87
|
|
64
88
|
def reference_type(reference)
|
65
|
-
klass =
|
89
|
+
klass = class_name.constantize.reflections[reference].klass
|
66
90
|
klass.columns.find { |col| col.name == klass.primary_key }.sql_type
|
67
91
|
rescue
|
68
92
|
"bigint"
|
69
93
|
end
|
70
94
|
|
95
|
+
def reference_table_name(reference)
|
96
|
+
class_name.constantize.reflections[reference].klass.table_name
|
97
|
+
rescue
|
98
|
+
reference.tableize
|
99
|
+
end
|
100
|
+
|
101
|
+
def foreign_key_name(reference)
|
102
|
+
class_name.constantize.reflections[reference].foreign_key
|
103
|
+
rescue
|
104
|
+
"#{reference}_id"
|
105
|
+
end
|
106
|
+
|
107
|
+
def add_check_constraint
|
108
|
+
return if options[:skip_check_constraint]
|
109
|
+
<<-RUBY
|
110
|
+
add_check_constraint(
|
111
|
+
:#{table_name},
|
112
|
+
"#{check_constraint}",
|
113
|
+
name: :#{arc}
|
114
|
+
)
|
115
|
+
RUBY
|
116
|
+
end
|
117
|
+
|
71
118
|
def check_constraint
|
72
119
|
reference_checks = belong_tos.map do |reference|
|
73
|
-
"CASE WHEN #{reference}
|
120
|
+
"CASE WHEN #{foreign_key_name(reference)} IS NULL THEN 0 ELSE 1 END"
|
74
121
|
end
|
75
122
|
condition = options[:optional] ? "<= 1" : "= 1"
|
76
123
|
"(#{reference_checks.join(" + ")}) #{condition}"
|
@@ -1,10 +1,6 @@
|
|
1
1
|
class <%= migration_class_name %> < ActiveRecord::Migration[<%= ActiveRecord::Migration.current_version %>]
|
2
2
|
def change
|
3
|
-
|
4
|
-
|
5
|
-
:<%= table_name %>,
|
6
|
-
"<%= check_constraint %>",
|
7
|
-
name: :<%= arc %>
|
8
|
-
)<% end %>
|
3
|
+
<%= add_references %>
|
4
|
+
<%= add_check_constraint %>
|
9
5
|
end
|
10
6
|
end
|
metadata
CHANGED
@@ -1,14 +1,14 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: activerecord-exclusive-arc
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 0.
|
4
|
+
version: 0.2.2
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- justin talbott
|
8
8
|
autorequire:
|
9
9
|
bindir: exe
|
10
10
|
cert_chain: []
|
11
|
-
date: 2023-04-
|
11
|
+
date: 2023-04-10 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
14
|
name: activerecord
|
@@ -50,26 +50,6 @@ dependencies:
|
|
50
50
|
- - "<"
|
51
51
|
- !ruby/object:Gem::Version
|
52
52
|
version: '8'
|
53
|
-
- !ruby/object:Gem::Dependency
|
54
|
-
name: railties
|
55
|
-
requirement: !ruby/object:Gem::Requirement
|
56
|
-
requirements:
|
57
|
-
- - ">="
|
58
|
-
- !ruby/object:Gem::Version
|
59
|
-
version: '6.1'
|
60
|
-
- - "<"
|
61
|
-
- !ruby/object:Gem::Version
|
62
|
-
version: '8'
|
63
|
-
type: :runtime
|
64
|
-
prerelease: false
|
65
|
-
version_requirements: !ruby/object:Gem::Requirement
|
66
|
-
requirements:
|
67
|
-
- - ">="
|
68
|
-
- !ruby/object:Gem::Version
|
69
|
-
version: '6.1'
|
70
|
-
- - "<"
|
71
|
-
- !ruby/object:Gem::Version
|
72
|
-
version: '8'
|
73
53
|
description:
|
74
54
|
email:
|
75
55
|
- gmail@justintalbott.com
|