familyable 0.0.1

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: 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: []