rails_dynamic_associations 0.1.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 ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA1:
3
+ metadata.gz: 02f1e9739f27b665443124374994afc6729e4e8f
4
+ data.tar.gz: f5d07be362c9b98969dfe4b5b0109f6b429f2c0f
5
+ SHA512:
6
+ metadata.gz: 5ee36ecaeee3642a97e3875c93c365e7323ac01ecc6fd7db0f2e67310eaf3a23aa61d9a0e351ee88b5c98f7de8d676f31fa456a1d992167dd8e37d4a9e9fd6ec
7
+ data.tar.gz: fc3ce8c1acbdebba32efd429e06eb5fc0b3390fc72163eb28905730f0a6293f5e0222fd67b2d89b8045a87aa138157f7b43b105bb070a7581efda2e0a0422802
data/MIT-LICENSE ADDED
@@ -0,0 +1,20 @@
1
+ Copyright 2013 Alexander Senko, SoftPro Ltd.
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,93 @@
1
+ # RailsDynamicAssociations
2
+
3
+ Define your model associations in the database without changing the schema or models.
4
+
5
+ ## Features
6
+
7
+ * Creates associations for your models when application starts.
8
+ * Provides `Relation` & `Role` models.
9
+ * No configuration code needed.
10
+ * No code generated or inserted to your app (except migrations).
11
+ * Adds some useful methods to `ActiveRecord` objects to handle their relations.
12
+
13
+ ## Installation
14
+
15
+ 1. Add the gem to your `Gemfile` and `bundle` it.
16
+ 2. Copy migrations to your app (`rake rails_dynamic_associations:install:migrations`).
17
+ 3. Migrate the DB (`rake db:migrate`).
18
+
19
+ ## Usage
20
+
21
+ Add configuration records to the DB:
22
+
23
+ ``` ruby
24
+ Relation.create({
25
+ source_type: Person,
26
+ target_type: Book,
27
+ })
28
+ ```
29
+
30
+ Or use a helper method:
31
+
32
+ ``` ruby
33
+ Relation.seed Person, Book
34
+ ```
35
+
36
+ Now you have:
37
+
38
+ ``` ruby
39
+ person.books
40
+ book.people
41
+ ```
42
+
43
+ ### Roles
44
+
45
+ You can create multiple role-based associations between two models.
46
+
47
+ ``` ruby
48
+ Relation.seed Person, Book, %w[
49
+ author
50
+ editor
51
+ ]
52
+ ```
53
+
54
+ You will get:
55
+
56
+ ``` ruby
57
+ person.books
58
+ person.authored_books
59
+ person.edited_books
60
+
61
+ book.people
62
+ book.author_people
63
+ book.editor_people
64
+ ```
65
+
66
+ #### `User` special case
67
+
68
+ In case you have set up relations with a `User` model you'll get a slightly different naming:
69
+
70
+ ``` ruby
71
+ Relation.seed User, Book, %w[
72
+ author
73
+ editor
74
+ ]
75
+ ```
76
+
77
+ ``` ruby
78
+ book.users
79
+ book.authors
80
+ book.editors
81
+ ```
82
+
83
+ ###### TODO
84
+
85
+ * Describe self-referential associations.
86
+
87
+ ## Contributing
88
+
89
+ 1. Fork it
90
+ 2. Create your feature branch (`git checkout -b my-new-feature`)
91
+ 3. Commit your changes (`git commit -am 'Add some feature'`)
92
+ 4. Push to the branch (`git push origin my-new-feature`)
93
+ 5. Create new Pull Request
data/Rakefile ADDED
@@ -0,0 +1,25 @@
1
+ #!/usr/bin/env rake
2
+ begin
3
+ require 'bundler/setup'
4
+ rescue LoadError
5
+ puts 'You must `gem install bundler` and `bundle install` to run rake tasks'
6
+ end
7
+ begin
8
+ require 'rdoc/task'
9
+ rescue LoadError
10
+ require 'rdoc/rdoc'
11
+ require 'rake/rdoctask'
12
+ RDoc::Task = Rake::RDocTask
13
+ end
14
+
15
+ RDoc::Task.new(:rdoc) do |rdoc|
16
+ rdoc.rdoc_dir = 'rdoc'
17
+ rdoc.title = 'RailsDynamicAssociations'
18
+ rdoc.options << '--line-numbers'
19
+ rdoc.rdoc_files.include('README.md')
20
+ rdoc.rdoc_files.include('lib/**/*.rb')
21
+ end
22
+
23
+
24
+ Bundler::GemHelper.install_tasks
25
+
@@ -0,0 +1,78 @@
1
+ class Relation < ActiveRecord::Base
2
+ belongs_to :source, polymorphic: true
3
+ belongs_to :target, polymorphic: true
4
+ belongs_to :role
5
+
6
+ delegate :name, to: :role, allow_nil: true
7
+
8
+ RailsDynamicAssociations.directions.each &-> (attribute, direction) do
9
+ scope "#{direction}_abstract", -> (object = nil) {
10
+ if object then
11
+ send direction, object
12
+ else
13
+ all
14
+ end.
15
+ where "#{attribute}_id" => nil
16
+ }
17
+
18
+ scope "#{direction}_general", -> {
19
+ send("#{direction}_abstract").
20
+ where "#{attribute}_type" => nil
21
+ }
22
+
23
+ scope direction, -> (object) {
24
+ case object
25
+ when nil then
26
+ all
27
+ when Symbol then
28
+ send "#{direction}_#{object}"
29
+ when Class then
30
+ where "#{attribute}_type" => object.base_class
31
+ else
32
+ where "#{attribute}_type" => object.class.base_class,
33
+ "#{attribute}_id" => object.id
34
+ end
35
+ }
36
+ end
37
+
38
+ scope :abstract, -> {
39
+ of_abstract.to_abstract
40
+ }
41
+
42
+ scope :applied, -> {
43
+ where.not source_id: nil,
44
+ target_id: nil
45
+ }
46
+
47
+ scope :named, -> (*names) {
48
+ if names.present? then
49
+ where roles: { name: names.flatten.map(&:to_s) }
50
+ else
51
+ where.not roles: { name: nil }
52
+ end.
53
+ includes :role
54
+ }
55
+
56
+ def self.seed source, target, roles = nil
57
+ (roles.present? ? by_roles(roles) : [ self ]).map do |scope|
58
+ scope.create source_type: source,
59
+ target_type: target
60
+ end
61
+ end
62
+
63
+ def self.by_roles *names
64
+ Role.find_or_create_named(*names).
65
+ map &:relations
66
+ end
67
+
68
+
69
+ # Using polymorphic associations in combination with single table inheritance (STI) is
70
+ # a little tricky. In order for the associations to work as expected, ensure that you
71
+ # store the base model for the STI models in the type column of the polymorphic
72
+ # association.
73
+ for reflection in reflections.values.select { |r| r.options[:polymorphic] } do
74
+ define_method "#{reflection.name}_type=" do |type|
75
+ super type && type.to_s.classify.constantize.base_class.to_s
76
+ end
77
+ end
78
+ end
@@ -0,0 +1,32 @@
1
+ class Role < ActiveRecord::Base
2
+ has_many :relations
3
+
4
+ validates :name, presence: true, uniqueness: true
5
+
6
+ scope :named, -> (*names) {
7
+ where name: names.flatten.map(&:to_s)
8
+ }
9
+
10
+ scope :available, -> {
11
+ includes(:relations).
12
+ where relations: { id: Relation.of_abstract } # TODO: simplify
13
+ }
14
+
15
+ scope :in, -> (object) {
16
+ where relations: { id: object.source_relations } # TODO: simplify
17
+ }
18
+
19
+ scope :for, -> (subject) {
20
+ where relations: { id: subject.target_relations } # TODO: simplify
21
+ }
22
+
23
+ def self.find_or_create_named *names
24
+ names.flatten!
25
+ names.compact!
26
+
27
+ (existing = named(names)).all +
28
+ (names - existing.map(&:name)).map { |name|
29
+ create name: name
30
+ }
31
+ end
32
+ end
@@ -0,0 +1,9 @@
1
+ ActiveSupport.on_load :model_class do
2
+ for type in RailsDynamicAssociations.directions.keys do
3
+ for relation in send("#{type}_relations").abstract.select(&:"#{type}_type") do
4
+ setup_relation type, relation.send("#{type}_type").constantize, relation.role do |association|
5
+ attr_accessible "#{association.to_s.singularize}_ids"
6
+ end
7
+ end
8
+ end if self != Relation and Relation.table_exists? # needed for DB migrations & schema initializing
9
+ end
@@ -0,0 +1,10 @@
1
+ class CreateRoles < ActiveRecord::Migration
2
+ def change
3
+ create_table :roles do |t|
4
+ t.string :name
5
+ t.timestamps
6
+ end
7
+
8
+ add_index :roles, :name, unique: true
9
+ end
10
+ end
@@ -0,0 +1,14 @@
1
+ class CreateRelations < ActiveRecord::Migration
2
+ def change
3
+ create_table :relations do |t|
4
+ t.references :source, polymorphic: { default: 'User' }
5
+ t.references :target, polymorphic: true
6
+ t.references :role
7
+
8
+ t.timestamps
9
+ end
10
+
11
+ add_index :relations, [ :source_id, :source_type, :target_id, :target_type, :role_id ], unique: true,
12
+ name: 'index_relations_on_source_and_target_and_role'
13
+ end
14
+ end
@@ -0,0 +1,8 @@
1
+ ##
2
+ # TODO: refactor
3
+ #
4
+ class String
5
+ def passivize
6
+ sub(/(e?d?|[eo]r|ant)$/, 'ed')
7
+ end unless method_defined? :passivize
8
+ end
@@ -0,0 +1,101 @@
1
+ require 'active_support/concern'
2
+
3
+ module RailsDynamicAssociations::ActiveRecord
4
+ module Associations
5
+ extend ActiveSupport::Concern
6
+
7
+ module ClassMethods
8
+ protected
9
+
10
+ def setup_relation type, target = self, role = nil
11
+ define_association type, target
12
+ define_association type, target, role if role
13
+
14
+ for association, method in RailsDynamicAssociations.self_referential_recursive do
15
+ define_recursive_methods association, method if association.in? reflections and not method_defined? method
16
+ end
17
+ end
18
+
19
+ private
20
+
21
+ def define_relations_association type, target = self, role = nil
22
+ :"#{role ? association_name(type, target, role).to_s.singularize : type}_relations".tap do |association|
23
+ unless association.in? reflections then
24
+ has_many association, conditions: role && { role_id: role.id },
25
+ as: (RailsDynamicAssociations.directions.keys - [ type ]).first, class_name: 'Relation'
26
+ end
27
+ end
28
+ end
29
+
30
+ def define_association type, target = self, role = nil
31
+ unless (association = association_name(type, target, role)).in? reflections then
32
+ has_many association,
33
+ through: define_relations_association(type, target, role),
34
+ source: type,
35
+ source_type: target.base_class.name,
36
+ class_name: target.name
37
+
38
+ define_association_with_roles association unless role
39
+
40
+ yield association if block_given?
41
+ end
42
+ end
43
+
44
+ def define_association_with_roles association
45
+ redefine_method "#{association}_with_roles" do |*roles|
46
+ send(association).where(
47
+ relations: {
48
+ role_id: Role.named(roles).pluck(:id)
49
+ }
50
+ )
51
+ end
52
+ end
53
+
54
+ def define_recursive_methods association, method, tree_method = "#{method.to_s.singularize}_tree", distance_method = "#{method}_with_distance"
55
+ redefine_method tree_method do
56
+ send(association).inject([]) { |tree, node|
57
+ tree << node << node.send(tree_method)
58
+ }.reject &:blank?
59
+ end
60
+
61
+ redefine_method distance_method do
62
+ (with_distance = -> (level, distance) {
63
+ if level.is_a? Array then
64
+ level.inject(ActiveSupport::OrderedHash.new) { |hash, node|
65
+ hash.merge with_distance[node, distance.next]
66
+ }
67
+ else
68
+ { level => distance }
69
+ end
70
+ })[send(tree_method), 0]
71
+ end
72
+
73
+ redefine_method method do
74
+ send(tree_method).flatten
75
+ end
76
+ end
77
+
78
+ def association_name type, target = self, role = nil
79
+ if role then
80
+ if target == self || target <= User then
81
+ {
82
+ source: role.name,
83
+ target: "#{role.name.passivize}_#{target.name.split('::').reverse.join}",
84
+ }[type]
85
+ else
86
+ "#{{
87
+ source: role.name,
88
+ target: role.name.passivize,
89
+ }[type]}_#{association_name type, target}"
90
+ end
91
+ else
92
+ if target == self then
93
+ RailsDynamicAssociations.self_referential_recursive[type].to_s
94
+ else
95
+ target.name.split('::').reverse.join
96
+ end
97
+ end.tableize.to_sym
98
+ end
99
+ end
100
+ end
101
+ end
@@ -0,0 +1,62 @@
1
+ require 'active_support/concern'
2
+
3
+ module RailsDynamicAssociations::ActiveRecord
4
+ module Relations
5
+ extend ActiveSupport::Concern
6
+
7
+ included do
8
+ extend ClassAndInstanceMethods
9
+ include ClassAndInstanceMethods
10
+ end
11
+
12
+ module ClassMethods
13
+ RailsDynamicAssociations.opposite_directions.each &-> (association, method) do
14
+ define_method "#{association}_relations" do
15
+ Relation.send method, self
16
+ end
17
+ end
18
+ end
19
+
20
+ module ClassAndInstanceMethods
21
+ # TODO: use keyword arguments
22
+ def find_relations args = {}
23
+ directions = RailsDynamicAssociations.directions
24
+
25
+ for association, method in directions do
26
+ args[association] = args.delete method
27
+ end
28
+
29
+ as = [ args[:as] ].flatten
30
+
31
+ if directions.keys.inject(nil) { |r, k| !r ^ !args[k] } then # direction specified
32
+ for association, method in directions do
33
+ next unless args[association]
34
+
35
+ return (as.present? ?
36
+ source_relations.named(as) :
37
+ source_relations
38
+ ).send method, args[association]
39
+ end
40
+ else # both directions
41
+ directions.map do |association, method|
42
+ as.present? ?
43
+ send("#{association}_relations").named(as) :
44
+ send("#{association}_relations")
45
+ end.sum
46
+ end
47
+ end
48
+
49
+ def relative? args = {}
50
+ find_relations(args).
51
+ present?
52
+ end
53
+
54
+ def relatives args = {}
55
+ find_relations(args).
56
+ map { |r|
57
+ ([ r.source, r.target ] - [ self ]).first
58
+ }.uniq
59
+ end
60
+ end
61
+ end
62
+ end
@@ -0,0 +1,24 @@
1
+ require 'active_support/concern'
2
+
3
+ module RailsDynamicAssociations::ActiveRecord
4
+ module Roles
5
+ extend ActiveSupport::Concern
6
+
7
+ included do
8
+ extend ClassAndInstanceMethods
9
+ include ClassAndInstanceMethods
10
+ end
11
+
12
+ module ClassAndInstanceMethods
13
+ def roles to: nil, &block
14
+ if block_given? then
15
+ source_relations.instance_eval &block
16
+ else
17
+ source_relations
18
+ end.
19
+ to(to).
20
+ map(&:name).uniq
21
+ end
22
+ end
23
+ end
24
+ end
@@ -0,0 +1,9 @@
1
+ require 'rails_dynamic_associations/active_record/associations'
2
+ require 'rails_dynamic_associations/active_record/relations'
3
+ require 'rails_dynamic_associations/active_record/roles'
4
+
5
+ ActiveSupport.on_load :active_record do
6
+ include RailsDynamicAssociations::ActiveRecord::Associations
7
+ include RailsDynamicAssociations::ActiveRecord::Relations
8
+ include RailsDynamicAssociations::ActiveRecord::Roles
9
+ end
@@ -0,0 +1,5 @@
1
+ module RailsDynamicAssociations
2
+ class Engine < ::Rails::Engine
3
+ isolate_namespace RailsDynamicAssociations
4
+ end
5
+ end
@@ -0,0 +1,3 @@
1
+ module RailsDynamicAssociations
2
+ VERSION = '0.1.0'
3
+ end
@@ -0,0 +1,35 @@
1
+ require 'rails_model_load_hook'
2
+
3
+ require 'rails_dynamic_associations/engine'
4
+ require 'core_ext/string'
5
+
6
+ module RailsDynamicAssociations
7
+ mattr_accessor :directions,
8
+ :self_referential,
9
+ :self_referential_recursive
10
+
11
+ self.directions = {
12
+ source: :of,
13
+ target: :to,
14
+ }
15
+
16
+ self.self_referential = {
17
+ source: :child,
18
+ target: :parent,
19
+ }
20
+
21
+ self.self_referential_recursive = {
22
+ parents: :ancestors,
23
+ children: :descendants,
24
+ }
25
+
26
+ def self.opposite_directions
27
+ directions.each_with_object({}) do |(key, value), hash|
28
+ hash[key] = directions.values.find do |v|
29
+ v != value
30
+ end
31
+ end
32
+ end
33
+ end
34
+
35
+ require 'rails_dynamic_associations/active_record'
@@ -0,0 +1,4 @@
1
+ # desc 'Explaining what the task does'
2
+ # task :rails_dynamic_associations do
3
+ # # Task goes here
4
+ # end
metadata ADDED
@@ -0,0 +1,104 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: rails_dynamic_associations
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.1.0
5
+ platform: ruby
6
+ authors:
7
+ - Alexander Senko
8
+ autorequire:
9
+ bindir: bin
10
+ cert_chain: []
11
+ date: 2015-01-27 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: '3.2'
20
+ type: :runtime
21
+ prerelease: false
22
+ version_requirements: !ruby/object:Gem::Requirement
23
+ requirements:
24
+ - - "~>"
25
+ - !ruby/object:Gem::Version
26
+ version: '3.2'
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'
34
+ type: :runtime
35
+ prerelease: false
36
+ version_requirements: !ruby/object:Gem::Requirement
37
+ requirements:
38
+ - - ">="
39
+ - !ruby/object:Gem::Version
40
+ version: '0'
41
+ - !ruby/object:Gem::Dependency
42
+ name: sqlite3
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
+ description: Define your model associations in the database without changing the schema
56
+ or models.
57
+ email:
58
+ - Alexander.Senko@gmail.com
59
+ executables: []
60
+ extensions: []
61
+ extra_rdoc_files: []
62
+ files:
63
+ - MIT-LICENSE
64
+ - README.md
65
+ - Rakefile
66
+ - app/models/relation.rb
67
+ - app/models/role.rb
68
+ - config/initializers/model_associations.rb
69
+ - db/migrate/01_create_roles.rb
70
+ - db/migrate/02_create_relations.rb
71
+ - lib/core_ext/string.rb
72
+ - lib/rails_dynamic_associations.rb
73
+ - lib/rails_dynamic_associations/active_record.rb
74
+ - lib/rails_dynamic_associations/active_record/associations.rb
75
+ - lib/rails_dynamic_associations/active_record/relations.rb
76
+ - lib/rails_dynamic_associations/active_record/roles.rb
77
+ - lib/rails_dynamic_associations/engine.rb
78
+ - lib/rails_dynamic_associations/version.rb
79
+ - lib/tasks/rails_dynamic_associations_tasks.rake
80
+ homepage: https://github.com/softpro/rails_dynamic_associations
81
+ licenses:
82
+ - MIT
83
+ metadata: {}
84
+ post_install_message:
85
+ rdoc_options: []
86
+ require_paths:
87
+ - lib
88
+ required_ruby_version: !ruby/object:Gem::Requirement
89
+ requirements:
90
+ - - ">="
91
+ - !ruby/object:Gem::Version
92
+ version: '0'
93
+ required_rubygems_version: !ruby/object:Gem::Requirement
94
+ requirements:
95
+ - - ">="
96
+ - !ruby/object:Gem::Version
97
+ version: '0'
98
+ requirements: []
99
+ rubyforge_project:
100
+ rubygems_version: 2.2.2
101
+ signing_key:
102
+ specification_version: 4
103
+ summary: DB-driven model associations for Rails.
104
+ test_files: []