adjustable_schema 0.5.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +7 -0
- data/CHANGELOG.md +0 -0
- data/MIT-LICENSE +20 -0
- data/README.md +104 -0
- data/Rakefile +8 -0
- data/app/assets/config/adjustable_schema_manifest.js +1 -0
- data/app/assets/stylesheets/adjustable_schema/application.css +15 -0
- data/app/controllers/adjustable_schema/application_controller.rb +4 -0
- data/app/helpers/adjustable_schema/application_helper.rb +4 -0
- data/app/jobs/adjustable_schema/application_job.rb +4 -0
- data/app/mailers/adjustable_schema/application_mailer.rb +6 -0
- data/app/models/adjustable_schema/application_record.rb +5 -0
- data/app/models/adjustable_schema/relationship/role.rb +21 -0
- data/app/models/adjustable_schema/relationship.rb +123 -0
- data/app/models/concerns/adjustable_schema/active_record/associations.rb +28 -0
- data/app/models/concerns/adjustable_schema/active_record/relationships.rb +16 -0
- data/app/views/layouts/adjustable_schema/application.html.erb +15 -0
- data/config/initializers/associations.rb +6 -0
- data/config/routes.rb +2 -0
- data/db/migrate/01_create_adjustable_schema_relationship_tables.rb +39 -0
- data/lib/adjustable_schema/active_record/association/naming.rb +63 -0
- data/lib/adjustable_schema/active_record/association.rb +46 -0
- data/lib/adjustable_schema/active_record.rb +10 -0
- data/lib/adjustable_schema/authors.rb +15 -0
- data/lib/adjustable_schema/config.rb +76 -0
- data/lib/adjustable_schema/engine.rb +20 -0
- data/lib/adjustable_schema/version.rb +3 -0
- data/lib/adjustable_schema.rb +15 -0
- data/lib/tasks/adjustable_schema_tasks.rake +4 -0
- metadata +117 -0
checksums.yaml
ADDED
@@ -0,0 +1,7 @@
|
|
1
|
+
---
|
2
|
+
SHA256:
|
3
|
+
metadata.gz: 3485d9747332369fa6717cfcc2f8340dedc75cb47a991c4f6f52f6838dd0bab6
|
4
|
+
data.tar.gz: 9805cfd3909951255df31007226bf699208d710c4fc74861df94cc80730a6130
|
5
|
+
SHA512:
|
6
|
+
metadata.gz: af22e30050202341aba9503e91382526e25db5955f0d4dc6582078a09f415a3461db50f39c06f7307f7ed35bb8bbfdc987533a1d51ba30fccb038b91bf9db034
|
7
|
+
data.tar.gz: 2080e9f4268d52ee0ec94cffe07ba747eac277d14917727ec0c6156e0dd6b498958365a16d308b257f6f55885c3cffbcdaa617bca424fea0c557ab5a2f5de4d1
|
data/CHANGELOG.md
ADDED
File without changes
|
data/MIT-LICENSE
ADDED
@@ -0,0 +1,20 @@
|
|
1
|
+
Copyright Alexander Senko
|
2
|
+
|
3
|
+
Permission is hereby granted, free of charge, to any person obtaining
|
4
|
+
a copy of this software and associated documentation files (the
|
5
|
+
"Software"), to deal in the Software without restriction, including
|
6
|
+
without limitation the rights to use, copy, modify, merge, publish,
|
7
|
+
distribute, sublicense, and/or sell copies of the Software, and to
|
8
|
+
permit persons to whom the Software is furnished to do so, subject to
|
9
|
+
the following conditions:
|
10
|
+
|
11
|
+
The above copyright notice and this permission notice shall be
|
12
|
+
included in all copies or substantial portions of the Software.
|
13
|
+
|
14
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
15
|
+
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
|
16
|
+
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
|
17
|
+
NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
|
18
|
+
LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
|
19
|
+
OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
|
20
|
+
WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
data/README.md
ADDED
@@ -0,0 +1,104 @@
|
|
1
|
+
# Adjustable Schema for Rails
|
2
|
+
|
3
|
+
Define your model associations in the database without changing the schema or models.
|
4
|
+
|
5
|
+
This Rails Engine was renamed and refactored from [Rails Dynamic Associations](https://github.com/Alexander-Senko/rails_dynamic_associations).
|
6
|
+
|
7
|
+
## Features
|
8
|
+
|
9
|
+
* Creates associations for your models when application starts.
|
10
|
+
* Provides `Relationship` & `Relationship::Role` models.
|
11
|
+
* No configuration code needed.
|
12
|
+
* No code generated or inserted to your app (except migrations).
|
13
|
+
* Adds some useful methods to `ActiveRecord` models to handle their relationships.
|
14
|
+
|
15
|
+
## Usage
|
16
|
+
|
17
|
+
Add configuration records to the DB:
|
18
|
+
|
19
|
+
``` ruby
|
20
|
+
AdjustableSchema::Relationship.create! source_type: 'Person',
|
21
|
+
target_type: 'Book'
|
22
|
+
```
|
23
|
+
|
24
|
+
Or use a helper method:
|
25
|
+
|
26
|
+
``` ruby
|
27
|
+
AdjustableSchema::Relationship.seed! Person => Book
|
28
|
+
```
|
29
|
+
|
30
|
+
Now you have:
|
31
|
+
|
32
|
+
``` ruby
|
33
|
+
person.books
|
34
|
+
book.people
|
35
|
+
```
|
36
|
+
|
37
|
+
### Roles
|
38
|
+
|
39
|
+
You can create multiple role-based associations between two models.
|
40
|
+
|
41
|
+
``` ruby
|
42
|
+
AdjustableSchema::Relationship.seed! Person => Book, roles: %w[author editor]
|
43
|
+
```
|
44
|
+
|
45
|
+
You will get:
|
46
|
+
|
47
|
+
``` ruby
|
48
|
+
person.books
|
49
|
+
person.authored_books
|
50
|
+
person.edited_books
|
51
|
+
|
52
|
+
book.people
|
53
|
+
book.author_people
|
54
|
+
book.editor_people
|
55
|
+
```
|
56
|
+
|
57
|
+
#### Special cases
|
58
|
+
|
59
|
+
In case you have set up relations with a `User` model you'll get a slightly different naming:
|
60
|
+
|
61
|
+
``` ruby
|
62
|
+
AdjustableSchema::Relationship.seed! User => Book, %w[author editor]
|
63
|
+
```
|
64
|
+
|
65
|
+
``` ruby
|
66
|
+
book.users
|
67
|
+
book.authors
|
68
|
+
book.editors
|
69
|
+
```
|
70
|
+
|
71
|
+
The list of models to be handled this way can be set with `actor_model_names` configuration parameter.
|
72
|
+
It includes `User` by default.
|
73
|
+
|
74
|
+
###### TODO
|
75
|
+
|
76
|
+
* Describe self-referential associations.
|
77
|
+
|
78
|
+
## Installation
|
79
|
+
Add this line to your application's Gemfile:
|
80
|
+
|
81
|
+
```ruby
|
82
|
+
gem "adjustable_schema"
|
83
|
+
```
|
84
|
+
|
85
|
+
And then execute:
|
86
|
+
```bash
|
87
|
+
$ bundle
|
88
|
+
```
|
89
|
+
|
90
|
+
Or install it yourself as:
|
91
|
+
```bash
|
92
|
+
$ gem install adjustable_schema
|
93
|
+
```
|
94
|
+
|
95
|
+
## Contributing
|
96
|
+
|
97
|
+
1. Fork it
|
98
|
+
2. Create your feature branch (`git checkout -b my-new-feature`)
|
99
|
+
3. Commit your changes (`git commit -am 'Add some feature'`)
|
100
|
+
4. Push to the branch (`git push origin my-new-feature`)
|
101
|
+
5. Create new Pull Request
|
102
|
+
|
103
|
+
## License
|
104
|
+
The gem is available as open source under the terms of the [MIT License](https://opensource.org/licenses/MIT).
|
data/Rakefile
ADDED
@@ -0,0 +1 @@
|
|
1
|
+
//= link_directory ../stylesheets/adjustable_schema .css
|
@@ -0,0 +1,15 @@
|
|
1
|
+
/*
|
2
|
+
* This is a manifest file that'll be compiled into application.css, which will include all the files
|
3
|
+
* listed below.
|
4
|
+
*
|
5
|
+
* Any CSS and SCSS file within this directory, lib/assets/stylesheets, vendor/assets/stylesheets,
|
6
|
+
* or any plugin's vendor/assets/stylesheets directory can be referenced here using a relative path.
|
7
|
+
*
|
8
|
+
* You're free to add application-wide styles to this file and they'll appear at the bottom of the
|
9
|
+
* compiled file so the styles you add here take precedence over styles defined in any other CSS/SCSS
|
10
|
+
* files in this directory. Styles in this file should be added after the last require_* statement.
|
11
|
+
* It is generally better to create a new file per style scope.
|
12
|
+
*
|
13
|
+
*= require_tree .
|
14
|
+
*= require_self
|
15
|
+
*/
|
@@ -0,0 +1,21 @@
|
|
1
|
+
module AdjustableSchema
|
2
|
+
class Relationship
|
3
|
+
class Role < ApplicationRecord
|
4
|
+
include Organizer::Identifiable.by :name
|
5
|
+
|
6
|
+
has_many :relationships
|
7
|
+
|
8
|
+
validates :name, presence: true, uniqueness: true
|
9
|
+
|
10
|
+
# FIXME: depends on default naming
|
11
|
+
scope :available, -> { with_relationships { of :abstract } }
|
12
|
+
scope :of, -> source { with_relationships { of source } }
|
13
|
+
scope :for, -> target { with_relationships { to target } }
|
14
|
+
|
15
|
+
def self.with_relationships(&)
|
16
|
+
joins(:relationships)
|
17
|
+
.merge Relationship.instance_eval(&)
|
18
|
+
end
|
19
|
+
end
|
20
|
+
end
|
21
|
+
end
|
@@ -0,0 +1,123 @@
|
|
1
|
+
module AdjustableSchema
|
2
|
+
class Relationship < ApplicationRecord
|
3
|
+
belongs_to :source, polymorphic: true, optional: true
|
4
|
+
belongs_to :target, polymorphic: true, optional: true
|
5
|
+
belongs_to :role, optional: true
|
6
|
+
|
7
|
+
default_scope do
|
8
|
+
references(:role)
|
9
|
+
.includes :role
|
10
|
+
end
|
11
|
+
|
12
|
+
Config.shortcuts.each &-> ((association, method)) do
|
13
|
+
scope method, -> object {
|
14
|
+
case object
|
15
|
+
when ::ActiveRecord::Base, nil
|
16
|
+
where association => object
|
17
|
+
when Class
|
18
|
+
where "#{association}_type" => object.ancestors
|
19
|
+
.select { _1 <= object.base_class }
|
20
|
+
.map(&:name)
|
21
|
+
when ::ActiveRecord::Relation
|
22
|
+
send(method, object.klass)
|
23
|
+
.where "#{association}_id" => object
|
24
|
+
when Symbol
|
25
|
+
send "#{method}_#{object}"
|
26
|
+
else
|
27
|
+
raise ArgumentError, "no relationships for #{object.inspect}"
|
28
|
+
end
|
29
|
+
}
|
30
|
+
|
31
|
+
scope "#{method}_abstract", -> object = nil {
|
32
|
+
if object
|
33
|
+
send(__method__).
|
34
|
+
send method, object
|
35
|
+
else
|
36
|
+
where "#{association}_id" => nil
|
37
|
+
end
|
38
|
+
}
|
39
|
+
end
|
40
|
+
|
41
|
+
scope :abstract, -> {
|
42
|
+
Config.shortcuts.values
|
43
|
+
.map { send _1, :abstract }
|
44
|
+
.reduce &:merge
|
45
|
+
}
|
46
|
+
|
47
|
+
scope :general, -> {
|
48
|
+
where target: nil
|
49
|
+
}
|
50
|
+
|
51
|
+
scope :applied, -> {
|
52
|
+
where.not(
|
53
|
+
source: nil,
|
54
|
+
target: nil,
|
55
|
+
)
|
56
|
+
}
|
57
|
+
|
58
|
+
scope :named, -> *names {
|
59
|
+
case names
|
60
|
+
when [] # i.e. `named`
|
61
|
+
where.not role: nil
|
62
|
+
else
|
63
|
+
with_roles { where name: names }
|
64
|
+
end
|
65
|
+
}
|
66
|
+
|
67
|
+
scope :nameless, -> { where role: nil }
|
68
|
+
|
69
|
+
def self.with_roles(&)
|
70
|
+
joins(:role)
|
71
|
+
.merge Role.instance_eval(&)
|
72
|
+
end
|
73
|
+
|
74
|
+
class << self
|
75
|
+
def seed! *models, roles: [], **_models
|
76
|
+
return seed!({ **Hash[*models], **_models }, roles:) if _models.any? # support keyword arguments syntax
|
77
|
+
|
78
|
+
case models
|
79
|
+
in [
|
80
|
+
String | Symbol | Class => source_type,
|
81
|
+
String | Symbol | Class => target_type,
|
82
|
+
]
|
83
|
+
roles
|
84
|
+
.map { |name| Role.find_or_create_by! name: }
|
85
|
+
.then { _1.presence or [ nil ] } # no roles => nameless relationship
|
86
|
+
.map { |role| create! source_type:, target_type:, role: }
|
87
|
+
in [ Hash => models ]
|
88
|
+
for sources, targets in models do
|
89
|
+
for source, target in Array(sources).product Array(targets) do
|
90
|
+
seed! source, target, roles:
|
91
|
+
end
|
92
|
+
end
|
93
|
+
in [ Class => source ]
|
94
|
+
seed! source, source, roles: # self-related
|
95
|
+
end
|
96
|
+
end
|
97
|
+
end
|
98
|
+
|
99
|
+
delegate :name, to: :role, allow_nil: true
|
100
|
+
|
101
|
+
def name= role_name
|
102
|
+
self.role =
|
103
|
+
role_name && Role[role_name]
|
104
|
+
end
|
105
|
+
|
106
|
+
def abstract? = not (source or target)
|
107
|
+
|
108
|
+
# HACK
|
109
|
+
# Using polymorphic associations in combination with single table inheritance (STI) is
|
110
|
+
# a little tricky. In order for the associations to work as expected, ensure that you
|
111
|
+
# store the base model for the STI models in the type column of the polymorphic
|
112
|
+
# association.
|
113
|
+
# https://api.rubyonrails.org/classes/ActiveRecord/Associations/ClassMethods.html#module-ActiveRecord::Associations::ClassMethods-label-Polymorphic+Associations
|
114
|
+
reflections
|
115
|
+
.values
|
116
|
+
.select { _1.options[:polymorphic] }
|
117
|
+
.each do |reflection|
|
118
|
+
define_method "#{reflection.name}_type=" do |type|
|
119
|
+
super type && type.to_s.classify.constantize.base_class.to_s
|
120
|
+
end
|
121
|
+
end
|
122
|
+
end
|
123
|
+
end
|
@@ -0,0 +1,28 @@
|
|
1
|
+
module AdjustableSchema
|
2
|
+
module ActiveRecord
|
3
|
+
concern :Associations do
|
4
|
+
class_methods do
|
5
|
+
private
|
6
|
+
|
7
|
+
def adjust_associations
|
8
|
+
relationships.each do |direction, relationships|
|
9
|
+
relationships
|
10
|
+
.select(&:"#{direction}_type")
|
11
|
+
.each do |relationship|
|
12
|
+
setup_association direction, relationship.send("#{direction}_type").constantize, relationship.role
|
13
|
+
end
|
14
|
+
end
|
15
|
+
end
|
16
|
+
|
17
|
+
def setup_association direction, target = self, role = nil
|
18
|
+
adjustable_association(direction, target ).define
|
19
|
+
adjustable_association(direction, target, role).define if role
|
20
|
+
end
|
21
|
+
|
22
|
+
def adjustable_association(...)
|
23
|
+
Association.new(self, ...)
|
24
|
+
end
|
25
|
+
end
|
26
|
+
end
|
27
|
+
end
|
28
|
+
end
|
@@ -0,0 +1,16 @@
|
|
1
|
+
module AdjustableSchema
|
2
|
+
module ActiveRecord
|
3
|
+
concern :Relationships do
|
4
|
+
class_methods do
|
5
|
+
def relationships
|
6
|
+
@relationships ||= # cache
|
7
|
+
Config.association_directions.to_h do
|
8
|
+
[ _1, Relationship.abstract.send(Config.shortcuts.opposite[_1], self) ]
|
9
|
+
end
|
10
|
+
end
|
11
|
+
|
12
|
+
def roles(&) = Role.of self, &
|
13
|
+
end
|
14
|
+
end
|
15
|
+
end
|
16
|
+
end
|
@@ -0,0 +1,15 @@
|
|
1
|
+
<!DOCTYPE html>
|
2
|
+
<html>
|
3
|
+
<head>
|
4
|
+
<title>Adjustable schema</title>
|
5
|
+
<%= csrf_meta_tags %>
|
6
|
+
<%= csp_meta_tag %>
|
7
|
+
|
8
|
+
<%= stylesheet_link_tag "adjustable_schema/application", media: "all" %>
|
9
|
+
</head>
|
10
|
+
<body>
|
11
|
+
|
12
|
+
<%= yield %>
|
13
|
+
|
14
|
+
</body>
|
15
|
+
</html>
|
data/config/routes.rb
ADDED
@@ -0,0 +1,39 @@
|
|
1
|
+
class CreateAdjustableSchemaRelationshipTables < ActiveRecord::Migration[7.1]
|
2
|
+
def change
|
3
|
+
# Use Active Record's configured type for primary and foreign keys
|
4
|
+
primary_key_type, foreign_key_type = primary_and_foreign_key_types
|
5
|
+
|
6
|
+
create_table :adjustable_schema_relationship_roles do |t|
|
7
|
+
t.string :name, index: { unique: true }
|
8
|
+
|
9
|
+
t.timestamps
|
10
|
+
end
|
11
|
+
|
12
|
+
create_table :adjustable_schema_relationships do |t|
|
13
|
+
t.references :source, polymorphic: true, index: true, type: foreign_key_type
|
14
|
+
t.references :target, polymorphic: true, index: true, type: foreign_key_type
|
15
|
+
t.references :role, index: true,
|
16
|
+
foreign_key: { to_table: :adjustable_schema_relationship_roles }
|
17
|
+
|
18
|
+
t.timestamps
|
19
|
+
|
20
|
+
t.index %i[
|
21
|
+
source_id source_type
|
22
|
+
target_id target_type
|
23
|
+
role_id
|
24
|
+
].tap { |columns|
|
25
|
+
columns.reject! { _1.ends_with? '_type' } if foreign_key_type == :uuid # OPTIMIZATION: IDs are unique
|
26
|
+
}, unique: true, name: :index_adjustable_schema_relationships_uniqueness
|
27
|
+
end
|
28
|
+
end
|
29
|
+
|
30
|
+
private
|
31
|
+
|
32
|
+
def primary_and_foreign_key_types
|
33
|
+
config = Rails.configuration.generators
|
34
|
+
setting = config.options[config.orm][:primary_key_type]
|
35
|
+
primary_key_type = setting || :primary_key
|
36
|
+
foreign_key_type = setting || :bigint
|
37
|
+
[primary_key_type, foreign_key_type]
|
38
|
+
end
|
39
|
+
end
|
@@ -0,0 +1,63 @@
|
|
1
|
+
module AdjustableSchema
|
2
|
+
module ActiveRecord
|
3
|
+
class Association
|
4
|
+
concerning :Naming do
|
5
|
+
module Inflections
|
6
|
+
refine String do
|
7
|
+
def passivize
|
8
|
+
sub(/(e?d?|ing|[eo]r|ant|(t)ion)$/, '\\2ed')
|
9
|
+
end
|
10
|
+
end
|
11
|
+
end
|
12
|
+
|
13
|
+
using Inflections
|
14
|
+
|
15
|
+
def name
|
16
|
+
@name ||= # cache
|
17
|
+
(role ? name_with_role : name_without_role)
|
18
|
+
.to_s
|
19
|
+
.tableize
|
20
|
+
.to_sym
|
21
|
+
end
|
22
|
+
|
23
|
+
def target_name
|
24
|
+
@target_name ||= # cache
|
25
|
+
target.name
|
26
|
+
.split('::')
|
27
|
+
.reverse
|
28
|
+
.join
|
29
|
+
.underscore
|
30
|
+
end
|
31
|
+
|
32
|
+
def relationships_name = :"#{role ? name_with_role : direction}_relationships"
|
33
|
+
|
34
|
+
private
|
35
|
+
|
36
|
+
def name_with_role
|
37
|
+
@name_with_role ||= # cache
|
38
|
+
if self_targeted?
|
39
|
+
{
|
40
|
+
source: role.name,
|
41
|
+
target: "#{role.name.passivize}_#{target_name}",
|
42
|
+
}[direction]
|
43
|
+
else
|
44
|
+
"#{{
|
45
|
+
source: role.name,
|
46
|
+
target: role.name.passivize,
|
47
|
+
}[direction]}_#{target_name}"
|
48
|
+
end
|
49
|
+
end
|
50
|
+
|
51
|
+
def name_without_role
|
52
|
+
@name_without_role ||= # cache
|
53
|
+
if self_targeted?
|
54
|
+
Config.association_directions
|
55
|
+
.self_related[direction]
|
56
|
+
else
|
57
|
+
target_name
|
58
|
+
end
|
59
|
+
end
|
60
|
+
end
|
61
|
+
end
|
62
|
+
end
|
63
|
+
end
|
@@ -0,0 +1,46 @@
|
|
1
|
+
module AdjustableSchema
|
2
|
+
module ActiveRecord
|
3
|
+
class Association < Struct.new(:owner, :direction, :target, :role)
|
4
|
+
require_relative 'association/naming'
|
5
|
+
|
6
|
+
def define
|
7
|
+
name.tap do |association_name|
|
8
|
+
has_many association_name,
|
9
|
+
through: define_relationships,
|
10
|
+
source: direction,
|
11
|
+
source_type: target.base_class.name,
|
12
|
+
class_name: target.name
|
13
|
+
|
14
|
+
define_role_methods unless role
|
15
|
+
end
|
16
|
+
end
|
17
|
+
|
18
|
+
def self_targeted? = target == owner
|
19
|
+
|
20
|
+
private
|
21
|
+
|
22
|
+
def define_relationships
|
23
|
+
relationships_name.tap do |association_name|
|
24
|
+
has_many association_name, role && -> { where role: },
|
25
|
+
as: Config.association_directions.opposite(to: direction),
|
26
|
+
class_name: 'AdjustableSchema::Relationship'
|
27
|
+
end
|
28
|
+
end
|
29
|
+
|
30
|
+
def define_role_methods
|
31
|
+
name = self.name
|
32
|
+
|
33
|
+
owner.redefine_method "#{name}_with_roles" do |*roles|
|
34
|
+
send(name)
|
35
|
+
.merge Relationship.named *roles
|
36
|
+
end
|
37
|
+
end
|
38
|
+
|
39
|
+
def has_many(association_name, ...)
|
40
|
+
return if owner.reflect_on_association association_name
|
41
|
+
|
42
|
+
owner.has_many(association_name, ...)
|
43
|
+
end
|
44
|
+
end
|
45
|
+
end
|
46
|
+
end
|
@@ -0,0 +1,15 @@
|
|
1
|
+
Gem::Author ||= Struct.new(
|
2
|
+
:name,
|
3
|
+
:email,
|
4
|
+
:github_url,
|
5
|
+
)
|
6
|
+
|
7
|
+
module AdjustableSchema
|
8
|
+
AUTHORS = [
|
9
|
+
Gem::Author.new(
|
10
|
+
name: 'Alexander Senko',
|
11
|
+
email: 'Alexander.Senko@gmail.com',
|
12
|
+
github_url: 'https://github.com/Alexander-Senko',
|
13
|
+
),
|
14
|
+
]
|
15
|
+
end
|
@@ -0,0 +1,76 @@
|
|
1
|
+
module AdjustableSchema
|
2
|
+
module Config
|
3
|
+
module Naming
|
4
|
+
def shortcuts
|
5
|
+
@shortcuts ||= # cache
|
6
|
+
config(:shortcut).tap do |shortcuts|
|
7
|
+
def shortcuts.opposite to: nil
|
8
|
+
if to
|
9
|
+
values.reject { _1 == to }.sole
|
10
|
+
else
|
11
|
+
transform_values { opposite to: _1 }
|
12
|
+
end
|
13
|
+
end
|
14
|
+
end
|
15
|
+
end
|
16
|
+
|
17
|
+
def self_related = config :self_related
|
18
|
+
|
19
|
+
def recursive
|
20
|
+
config.values.to_h do
|
21
|
+
[ _1[:self_related].to_s.pluralize.to_sym, _1[:recursive] ]
|
22
|
+
end
|
23
|
+
end
|
24
|
+
|
25
|
+
def opposite to:
|
26
|
+
reject { _1 == to }.sole
|
27
|
+
end
|
28
|
+
|
29
|
+
private
|
30
|
+
|
31
|
+
def config section = nil
|
32
|
+
if section
|
33
|
+
config.transform_values { _1[section] }
|
34
|
+
else
|
35
|
+
Config.association_names # TODO: DRY
|
36
|
+
end
|
37
|
+
end
|
38
|
+
end
|
39
|
+
|
40
|
+
module_function
|
41
|
+
|
42
|
+
def association_directions
|
43
|
+
@association_directions ||= # cache
|
44
|
+
association_names.keys.tap do |directions|
|
45
|
+
class << directions
|
46
|
+
include Naming
|
47
|
+
end
|
48
|
+
end
|
49
|
+
end
|
50
|
+
|
51
|
+
def find_direction(...)
|
52
|
+
normalize(...)
|
53
|
+
.find { |dir, *| dir.in? association_directions }
|
54
|
+
end
|
55
|
+
|
56
|
+
for method in %i[
|
57
|
+
shortcuts
|
58
|
+
] do
|
59
|
+
delegate method, to: :association_directions
|
60
|
+
module_function method
|
61
|
+
end
|
62
|
+
|
63
|
+
private
|
64
|
+
module_function
|
65
|
+
|
66
|
+
def association_names = Engine.config.names[:associations]
|
67
|
+
|
68
|
+
def normalize **options
|
69
|
+
shortcuts
|
70
|
+
.select { _2.in? options }
|
71
|
+
.each { options[_1] = options.delete _2 }
|
72
|
+
|
73
|
+
options
|
74
|
+
end
|
75
|
+
end
|
76
|
+
end
|
@@ -0,0 +1,20 @@
|
|
1
|
+
module AdjustableSchema
|
2
|
+
class Engine < ::Rails::Engine
|
3
|
+
isolate_namespace AdjustableSchema
|
4
|
+
|
5
|
+
config.names = {
|
6
|
+
associations: {
|
7
|
+
source: {
|
8
|
+
shortcut: :of,
|
9
|
+
self_related: :child,
|
10
|
+
recursive: :descendants,
|
11
|
+
},
|
12
|
+
target: {
|
13
|
+
shortcut: :to,
|
14
|
+
self_related: :parent,
|
15
|
+
recursive: :ancestors,
|
16
|
+
},
|
17
|
+
},
|
18
|
+
}
|
19
|
+
end
|
20
|
+
end
|
@@ -0,0 +1,15 @@
|
|
1
|
+
require "adjustable_schema/version"
|
2
|
+
require "adjustable_schema/engine"
|
3
|
+
require 'adjustable_schema/active_record'
|
4
|
+
|
5
|
+
require 'rails_model_load_hook' # should be loaded
|
6
|
+
|
7
|
+
module AdjustableSchema
|
8
|
+
autoload :Config, 'adjustable_schema/config'
|
9
|
+
|
10
|
+
module_function
|
11
|
+
|
12
|
+
def available?
|
13
|
+
Relationship.table_exists?
|
14
|
+
end
|
15
|
+
end
|
metadata
ADDED
@@ -0,0 +1,117 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: adjustable_schema
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
version: 0.5.0
|
5
|
+
platform: ruby
|
6
|
+
authors:
|
7
|
+
- Alexander Senko
|
8
|
+
autorequire:
|
9
|
+
bindir: bin
|
10
|
+
cert_chain: []
|
11
|
+
date: 2023-12-29 00:00:00.000000000 Z
|
12
|
+
dependencies:
|
13
|
+
- !ruby/object:Gem::Dependency
|
14
|
+
name: rails
|
15
|
+
requirement: !ruby/object:Gem::Requirement
|
16
|
+
requirements:
|
17
|
+
- - "~>"
|
18
|
+
- !ruby/object:Gem::Version
|
19
|
+
version: '7.1'
|
20
|
+
type: :runtime
|
21
|
+
prerelease: false
|
22
|
+
version_requirements: !ruby/object:Gem::Requirement
|
23
|
+
requirements:
|
24
|
+
- - "~>"
|
25
|
+
- !ruby/object:Gem::Version
|
26
|
+
version: '7.1'
|
27
|
+
- !ruby/object:Gem::Dependency
|
28
|
+
name: rails_model_load_hook
|
29
|
+
requirement: !ruby/object:Gem::Requirement
|
30
|
+
requirements:
|
31
|
+
- - "~>"
|
32
|
+
- !ruby/object:Gem::Version
|
33
|
+
version: '0.2'
|
34
|
+
type: :runtime
|
35
|
+
prerelease: false
|
36
|
+
version_requirements: !ruby/object:Gem::Requirement
|
37
|
+
requirements:
|
38
|
+
- - "~>"
|
39
|
+
- !ruby/object:Gem::Version
|
40
|
+
version: '0.2'
|
41
|
+
- !ruby/object:Gem::Dependency
|
42
|
+
name: organizer-rails
|
43
|
+
requirement: !ruby/object:Gem::Requirement
|
44
|
+
requirements:
|
45
|
+
- - ">="
|
46
|
+
- !ruby/object:Gem::Version
|
47
|
+
version: '0'
|
48
|
+
type: :runtime
|
49
|
+
prerelease: false
|
50
|
+
version_requirements: !ruby/object:Gem::Requirement
|
51
|
+
requirements:
|
52
|
+
- - ">="
|
53
|
+
- !ruby/object:Gem::Version
|
54
|
+
version: '0'
|
55
|
+
description: Rails Engine to allow ActiveRecord associations be set up in the DB instead
|
56
|
+
of being hard-coded.
|
57
|
+
email:
|
58
|
+
- Alexander.Senko@gmail.com
|
59
|
+
executables: []
|
60
|
+
extensions: []
|
61
|
+
extra_rdoc_files: []
|
62
|
+
files:
|
63
|
+
- CHANGELOG.md
|
64
|
+
- MIT-LICENSE
|
65
|
+
- README.md
|
66
|
+
- Rakefile
|
67
|
+
- app/assets/config/adjustable_schema_manifest.js
|
68
|
+
- app/assets/stylesheets/adjustable_schema/application.css
|
69
|
+
- app/controllers/adjustable_schema/application_controller.rb
|
70
|
+
- app/helpers/adjustable_schema/application_helper.rb
|
71
|
+
- app/jobs/adjustable_schema/application_job.rb
|
72
|
+
- app/mailers/adjustable_schema/application_mailer.rb
|
73
|
+
- app/models/adjustable_schema/application_record.rb
|
74
|
+
- app/models/adjustable_schema/relationship.rb
|
75
|
+
- app/models/adjustable_schema/relationship/role.rb
|
76
|
+
- app/models/concerns/adjustable_schema/active_record/associations.rb
|
77
|
+
- app/models/concerns/adjustable_schema/active_record/relationships.rb
|
78
|
+
- app/views/layouts/adjustable_schema/application.html.erb
|
79
|
+
- config/initializers/associations.rb
|
80
|
+
- config/routes.rb
|
81
|
+
- db/migrate/01_create_adjustable_schema_relationship_tables.rb
|
82
|
+
- lib/adjustable_schema.rb
|
83
|
+
- lib/adjustable_schema/active_record.rb
|
84
|
+
- lib/adjustable_schema/active_record/association.rb
|
85
|
+
- lib/adjustable_schema/active_record/association/naming.rb
|
86
|
+
- lib/adjustable_schema/authors.rb
|
87
|
+
- lib/adjustable_schema/config.rb
|
88
|
+
- lib/adjustable_schema/engine.rb
|
89
|
+
- lib/adjustable_schema/version.rb
|
90
|
+
- lib/tasks/adjustable_schema_tasks.rake
|
91
|
+
homepage: https://github.com/Alexander-Senko/adjustable_schema
|
92
|
+
licenses:
|
93
|
+
- MIT
|
94
|
+
metadata:
|
95
|
+
homepage_uri: https://github.com/Alexander-Senko/adjustable_schema
|
96
|
+
source_code_uri: https://github.com/Alexander-Senko/adjustable_schema
|
97
|
+
changelog_uri: https://github.com/Alexander-Senko/adjustable_schema/CHANGELOG.md
|
98
|
+
post_install_message:
|
99
|
+
rdoc_options: []
|
100
|
+
require_paths:
|
101
|
+
- lib
|
102
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
103
|
+
requirements:
|
104
|
+
- - ">="
|
105
|
+
- !ruby/object:Gem::Version
|
106
|
+
version: '3.2'
|
107
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
108
|
+
requirements:
|
109
|
+
- - ">="
|
110
|
+
- !ruby/object:Gem::Version
|
111
|
+
version: '0'
|
112
|
+
requirements: []
|
113
|
+
rubygems_version: 3.5.0.dev
|
114
|
+
signing_key:
|
115
|
+
specification_version: 4
|
116
|
+
summary: Adjustable data schemas for Rails
|
117
|
+
test_files: []
|