familyable 0.0.1

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA1:
3
+ metadata.gz: c05a813418d27a79b6a38a33d303ad1bd02e551d
4
+ data.tar.gz: 275d41cb50d00fb8f0a3512e712fb0fa979f9593
5
+ SHA512:
6
+ metadata.gz: ef6d2919bc506784be2d4b4b8770817994f10bebf506e2e0d3bdc1e41875444c6d41cfde7d04c87880efe02e15bd0f803efbd07c0273bbe5d8fdf78eeb1ea61a
7
+ data.tar.gz: b0bf6eb658ef5b07c1f802900f7db5a176c1edbd20be0e5916f75401551b9a763612b2fc534ed1d30efaffdadc1934b024dc2f4c234deb82dd33ca0beaa64c72
data/MIT-LICENSE ADDED
@@ -0,0 +1,20 @@
1
+ Copyright 2013 YOURNAME
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,82 @@
1
+ ### Familyable
2
+
3
+ This gem makes creating self-referential parent child relationships on a model easy. So for a `Person` model you have `person.parent` and `person.children` where the parent and children are also people.
4
+
5
+ You also get the following instance methods:
6
+
7
+ * descendents
8
+ * elders
9
+ * siblings
10
+ * family
11
+ * master *- the 'oldest' in the family*
12
+
13
+ and the class method:
14
+
15
+ * masters *- everyone without a parent*
16
+
17
+ Standard stuff I know but...
18
+ ##### Everyone of the methods above works with a single call to the data base!!!
19
+
20
+ This is a **[huge](https://github.com/brookisme/familyable-testapp)** performance gain. In fact, being able build the above methods with a single database call was the entire motivation for gemifiying something that otherwise was entirely straight forward. It should be noted that this was built the day after reading [this](http://hashrocket.com/blog/posts/recursive-sql-in-activerecord).
21
+
22
+ -----------------------------------------------------------
23
+
24
+ ##### WARNING: This project is still in development
25
+ [x] create relationship concern
26
+ [X] create generators for relationship models
27
+ [X] check that it works with engines
28
+ [ ] generate data for testapp
29
+ [ ] tests tests tests
30
+ [ ] refactor concern
31
+ [ ] add babies methods (class and instance)?
32
+
33
+
34
+ ### Requirments
35
+
36
+ You must be using a postgres data base... thats where the single-db-query magic happens.
37
+
38
+ ### Installation
39
+
40
+ familyable gem will be comming soon for now get it from this repo.. actually its too soon to use it - but soon!!!
41
+
42
+ ### Usage
43
+
44
+ -----------------------------------------------------------
45
+
46
+ Example: Adding Relationships to an existing `Person` model:
47
+
48
+ ##### Step 1: Generate Relationship Model
49
+
50
+ ```
51
+ $ bundle exec rails g familyable:relationships Person
52
+ $ bundle exec rake db:migrate
53
+ ```
54
+
55
+ Note: For use with Rails Engines use the full model name from the the root of your Engine directory.
56
+
57
+ ```
58
+ $ bundle exec rails g familyable:relationships MyEngine::Person
59
+ ```
60
+
61
+
62
+ ##### Step 2: Add Relationships Concern to Model
63
+
64
+ _app/models/person.rb_
65
+ ```ruby
66
+ class Person < ActiveRecord::Base
67
+ include Familyable::Relationships
68
+ ...
69
+ end
70
+ ```
71
+
72
+ ##### Step 3: You're done! start coding
73
+
74
+ quick note: all the instance methods above (accept for master) take an optional parameter *include\_self=false*. it does what you exactly what you think.
75
+
76
+ ```ruby
77
+ Person.masters
78
+ person.master
79
+ person.descendents
80
+ person.descendents(true)
81
+ ...
82
+ ```
data/Rakefile ADDED
@@ -0,0 +1,21 @@
1
+ begin
2
+ require 'bundler/setup'
3
+ rescue LoadError
4
+ puts 'You must `gem install bundler` and `bundle install` to run rake tasks'
5
+ end
6
+
7
+ require 'rdoc/task'
8
+
9
+ RDoc::Task.new(:rdoc) do |rdoc|
10
+ rdoc.rdoc_dir = 'rdoc'
11
+ rdoc.title = 'Familyable'
12
+ rdoc.options << '--line-numbers'
13
+ rdoc.rdoc_files.include('README.rdoc')
14
+ rdoc.rdoc_files.include('lib/**/*.rb')
15
+ end
16
+
17
+
18
+
19
+
20
+ Bundler::GemHelper.install_tasks
21
+
@@ -0,0 +1,258 @@
1
+ module Familyable
2
+ module Relationships
3
+ extend ActiveSupport::Concern
4
+
5
+
6
+
7
+ included do
8
+ has_many relationships_name.to_sym
9
+ has_many :children, through: relationships_name.to_sym
10
+ has_one inv_relationships_name.to_sym, class_name: relationship_class_name, foreign_key: "child_id"
11
+ has_one :parent, through: inv_relationships_name.to_sym, source: model_name.to_sym
12
+ end
13
+
14
+ # *****************************
15
+ #
16
+ # Main Interface
17
+ #
18
+ # *****************************
19
+
20
+ module ClassMethods
21
+ def masters
22
+ where(without_parents_where_sql)
23
+ end
24
+ end
25
+
26
+ def master
27
+ call = elders
28
+ call.where(klass.without_parents_where_sql).first
29
+ end
30
+
31
+ def descendents include_self=false
32
+ query_call(
33
+ "#{table_name}.id IN (#{descendents_sql_list})",
34
+ include_self
35
+ )
36
+ end
37
+
38
+ def elders include_self=false
39
+ query_call(
40
+ "#{table_name}.id IN (#{elders_sql_list})",
41
+ include_self
42
+ )
43
+ end
44
+
45
+ def siblings include_self=false
46
+ query_call(
47
+ "#{table_name}.id IN (#{siblings_sql_list})",
48
+ include_self
49
+ )
50
+ end
51
+
52
+ def family include_self=false
53
+ query_call(
54
+ "#{table_name}.id IN (#{family_sql_list})",
55
+ include_self
56
+ )
57
+ end
58
+
59
+ # *****************************
60
+ #
61
+ # Utilities
62
+ #
63
+ # *****************************
64
+
65
+ module ClassMethods
66
+
67
+ #
68
+ # ID LISTS
69
+ #
70
+
71
+ def children_of(id_sql)
72
+ id_sql = safe_identifier(id_sql)
73
+ tree_sql = <<-SQL
74
+ SELECT DISTINCT #{relationship_table_name}.child_id
75
+ FROM #{table_name}
76
+ JOIN #{relationship_table_name}
77
+ ON #{table_name}.id = #{relationship_table_name}.#{parent_field_name}
78
+ WHERE #{table_name}.id = #{id_sql}
79
+ SQL
80
+ end
81
+
82
+ def descendents_of(id_sql)
83
+ id_sql = safe_identifier(id_sql)
84
+ tree_sql = <<-SQL
85
+ WITH RECURSIVE search_tree(id, path) AS (
86
+ SELECT id, ARRAY[id]
87
+ FROM #{table_name}
88
+ WHERE id = #{id_sql}
89
+ UNION ALL
90
+ SELECT #{table_name}.id, path || #{table_name}.id
91
+ FROM search_tree
92
+ JOIN #{relationship_table_name} ON #{relationship_table_name}.#{parent_field_name} = search_tree.id
93
+ JOIN #{table_name} ON #{table_name}.id = #{relationship_table_name}.child_id
94
+ WHERE NOT #{table_name}.id = ANY(path)
95
+ )
96
+ SELECT id FROM search_tree ORDER BY path
97
+ SQL
98
+ end
99
+
100
+ def elders_of(id_sql)
101
+ id_sql = safe_identifier(id_sql)
102
+ tree_sql = <<-SQL
103
+ WITH RECURSIVE search_tree(id, path) AS (
104
+ SELECT id, ARRAY[id]
105
+ FROM #{table_name}
106
+ WHERE id = #{id_sql}
107
+ UNION ALL
108
+ SELECT #{table_name}.id, path || #{table_name}.id
109
+ FROM search_tree
110
+ JOIN #{relationship_table_name} ON #{relationship_table_name}.child_id = search_tree.id
111
+ JOIN #{table_name} ON #{table_name}.id = #{relationship_table_name}.#{parent_field_name}
112
+ WHERE NOT #{table_name}.id = ANY(path)
113
+ )
114
+ SELECT id FROM search_tree ORDER BY path
115
+ SQL
116
+ end
117
+
118
+ def without_parents_where_sql
119
+ "#{table_name}.id NOT IN (
120
+ SELECT DISTINCT #{relationship_table_name}.child_id
121
+ FROM #{relationship_table_name}
122
+ )"
123
+ end
124
+
125
+ #
126
+ # UTILS
127
+ #
128
+
129
+ def relationship_table_name
130
+ if @relationship_table_name.nil?
131
+ @relationship_table_name = "#{table_name.singularize}_relationships"
132
+ end
133
+ @relationship_table_name
134
+ end
135
+
136
+ def relationship_class_name
137
+ if @relationship_class_name.nil?
138
+ @relationship_class_name = "#{self.name.split("::").last}Relationship"
139
+ end
140
+ @relationship_class_name
141
+ end
142
+
143
+ def model_name
144
+ if @model_name.nil?
145
+ @model_name = "#{self.name.split("::").last.downcase}"
146
+ end
147
+ @model_name
148
+ end
149
+
150
+ def relationships_name
151
+ if @relationships_name.nil?
152
+ @relationships_name = "#{self.name.split("::").last.downcase}_relationships"
153
+ end
154
+ @relationships_name
155
+ end
156
+
157
+ def inv_relationships_name
158
+ if @inv_relationships_name.nil?
159
+ @inv_relationships_name = "inverse_#{relationships_name}"
160
+ end
161
+ @inv_relationships_name
162
+ end
163
+
164
+ def parent_field_name
165
+ if @parent_field_name.nil?
166
+ @parent_field_name = "#{self.name.split("::").last.downcase}_id"
167
+ end
168
+ @parent_field_name
169
+ end
170
+
171
+ def safe_identifier id_sql
172
+ if id_sql.to_i == 0
173
+ "(#{id_sql})"
174
+ else
175
+ id_sql
176
+ end
177
+ end
178
+ end
179
+
180
+
181
+ private
182
+
183
+ #
184
+ # SQL LISTS
185
+ #
186
+
187
+ def descendents_sql_list
188
+ klass.descendents_of(id)
189
+ end
190
+
191
+ def elders_sql_list
192
+ klass.elders_of(id)
193
+ end
194
+
195
+ def siblings_sql_list
196
+ klass.children_of(select_parent_sql)
197
+ end
198
+
199
+ def family_sql_list
200
+ klass.descendents_of(select_master_sql)
201
+ end
202
+
203
+ #
204
+ # SQL
205
+ #
206
+
207
+ def select_parent_sql
208
+ "SELECT #{relationship_table_name}.#{parent_field_name}
209
+ FROM #{klass.table_name}
210
+ JOIN #{relationship_table_name}
211
+ ON #{klass.table_name}.id = #{relationship_table_name}.#{parent_field_name}
212
+ WHERE #{relationship_table_name}.child_id = #{id}
213
+ LIMIT 1"
214
+ end
215
+
216
+
217
+ def select_master_sql
218
+ select_from_sql_list_and(klass.elders_of(id),klass.without_parents_where_sql)
219
+ end
220
+
221
+ #
222
+ # Utils
223
+ #
224
+
225
+ def query_call(where_sql,include_self=false)
226
+ call = self.class.where(where_sql)
227
+ call = call.where.not(id: id) unless include_self
228
+ call
229
+ end
230
+
231
+ def klass
232
+ @klass ||= self.class
233
+ end
234
+
235
+ def table_name
236
+ @table_name ||= klass.table_name
237
+ end
238
+
239
+ def relationship_table_name
240
+ @relationship_table_name ||= klass.relationship_table_name
241
+ end
242
+
243
+ def parent_field_name
244
+ @parent_field_name ||= klass.parent_field_name
245
+ end
246
+
247
+ def select_from_sql_list_and(sql_list,and_sql)
248
+ "SELECT #{klass.table_name}.id FROM #{klass.table_name}
249
+ WHERE (
250
+ #{klass.table_name}.id IN (
251
+ #{sql_list}
252
+ )
253
+ ) AND (
254
+ #{and_sql}
255
+ )"
256
+ end
257
+ end
258
+ end
@@ -0,0 +1,3 @@
1
+ module Familyable
2
+ VERSION = "0.0.1"
3
+ end
data/lib/familyable.rb ADDED
@@ -0,0 +1,3 @@
1
+ require 'familyable/concerns/relationships.rb'
2
+ module Familyable
3
+ end
@@ -0,0 +1,54 @@
1
+ require 'rails/generators'
2
+ module Familyable
3
+ class Generator < Rails::Generators::Base
4
+ desc "Shared options and methods for Familyable Generators"
5
+
6
+ argument :model_name, type: :string, required: true, desc: "model name"
7
+
8
+ private
9
+
10
+ def model_class_name
11
+ @model_name.camelize
12
+ end
13
+
14
+ def clean_model_class_name
15
+ model_class_name.split("::").last
16
+ end
17
+
18
+ def engine_name
19
+ parts = model_class_name.split("::")
20
+ if parts.length > 1
21
+ parts.pop()
22
+ parts.join("::")
23
+ end
24
+ end
25
+
26
+ def model_base_name
27
+ @model_name.underscore
28
+ end
29
+
30
+ def clean_model_base_name
31
+ model_base_name.split("/").last
32
+ end
33
+
34
+ def relationship_class_name
35
+ "#{model_class_name}Relationship"
36
+ end
37
+
38
+ def clean_relationship_class_name
39
+ "#{clean_model_class_name}Relationship"
40
+ end
41
+
42
+ def app_root_path
43
+ if engine_name.nil?
44
+ "#{Rails.root}"
45
+ else
46
+ "#{engine_name.constantize::Engine.root}"
47
+ end
48
+ end
49
+
50
+ def relationship_model_path
51
+ "#{app_root_path}/app/models/#{relationship_class_name.underscore}.rb"
52
+ end
53
+ end
54
+ end
@@ -0,0 +1,23 @@
1
+ require File.join(File.dirname(__FILE__), 'generator')
2
+ module Familyable
3
+ class RelationshipsGenerator < Familyable::Generator
4
+ desc "Options and methods for Familyable::Relationship"
5
+
6
+ argument :model_name, type: :string, required: true, desc: "model name"
7
+ class_option :delete, type: :boolean, required: false, default: false, desc: "delete relationship for model: defaut=false"
8
+
9
+ def generate_relationships
10
+ if options[:delete]
11
+ system("bundle exec rails d model #{clean_relationship_class_name}")
12
+ else
13
+ generate "model", "#{clean_relationship_class_name} #{clean_model_base_name}:references child:references"
14
+ end
15
+ end
16
+
17
+ def add_class_name
18
+ unless options[:delete]
19
+ gsub_file(relationship_model_path, "belongs_to :child", "belongs_to :child, class_name:\"#{clean_model_class_name}\"")
20
+ end
21
+ end
22
+ end
23
+ end
@@ -0,0 +1,4 @@
1
+ # desc "Explaining what the task does"
2
+ # task :familyable do
3
+ # # Task goes here
4
+ # end
metadata ADDED
@@ -0,0 +1,94 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: familyable
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.0.1
5
+ platform: ruby
6
+ authors:
7
+ - Brook Williams
8
+ autorequire:
9
+ bindir: bin
10
+ cert_chain: []
11
+ date: 2015-02-20 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: '4'
20
+ type: :runtime
21
+ prerelease: false
22
+ version_requirements: !ruby/object:Gem::Requirement
23
+ requirements:
24
+ - - "~>"
25
+ - !ruby/object:Gem::Version
26
+ version: '4'
27
+ - !ruby/object:Gem::Dependency
28
+ name: pg
29
+ requirement: !ruby/object:Gem::Requirement
30
+ requirements:
31
+ - - ">="
32
+ - !ruby/object:Gem::Version
33
+ version: '0'
34
+ type: :development
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: rspec-rails
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: A gem for creating self-referential parent child relationships on a model
56
+ email:
57
+ - brook.williams@gmail.com
58
+ executables: []
59
+ extensions: []
60
+ extra_rdoc_files: []
61
+ files:
62
+ - MIT-LICENSE
63
+ - README.md
64
+ - Rakefile
65
+ - lib/familyable.rb
66
+ - lib/familyable/concerns/relationships.rb
67
+ - lib/familyable/version.rb
68
+ - lib/generators/familyable/generator.rb
69
+ - lib/generators/familyable/relationships_generator.rb
70
+ - lib/tasks/familyable.rake
71
+ homepage: http://stickandlogdesigns.com
72
+ licenses: []
73
+ metadata: {}
74
+ post_install_message:
75
+ rdoc_options: []
76
+ require_paths:
77
+ - lib
78
+ required_ruby_version: !ruby/object:Gem::Requirement
79
+ requirements:
80
+ - - ">="
81
+ - !ruby/object:Gem::Version
82
+ version: '0'
83
+ required_rubygems_version: !ruby/object:Gem::Requirement
84
+ requirements:
85
+ - - ">="
86
+ - !ruby/object:Gem::Version
87
+ version: '0'
88
+ requirements: []
89
+ rubyforge_project:
90
+ rubygems_version: 2.2.2
91
+ signing_key:
92
+ specification_version: 4
93
+ summary: A gem for creating self-referential parent child relationships on a model
94
+ test_files: []