adjustable_schema 0.5.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/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: []
|