oakridgelanl-materialized-path 0.0.2

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.
Files changed (3) hide show
  1. data/README +39 -0
  2. data/lib/acts_as_materialized_path.rb +351 -0
  3. metadata +63 -0
data/README ADDED
@@ -0,0 +1,39 @@
1
+ ActsAsMaterializedPath
2
+ ======================
3
+
4
+ This mixin implements a set of trees (nested set) in a single table.
5
+
6
+ Within its limitations, this plugin is meant to be more efficient than
7
+ existing implementations of nested sets.
8
+
9
+
10
+ Example
11
+ =======
12
+
13
+ In the class implementing the trees:
14
+
15
+ acts_as_materialized_path :delimiter => '.',
16
+ :base => 10,
17
+ :column => 'materialized_path',
18
+ :places => 3,
19
+
20
+ The values show here are the defaults.
21
+
22
+
23
+ In the migration file, ensure the path_column_name is not null and has
24
+ a unique index:
25
+
26
+ t.string :materialized_path, :null => false
27
+
28
+ add_index(:tree_table, [:materialized_path], :unique => true)
29
+
30
+
31
+ In addition, the collating sequence for ascii representable
32
+ characters, c, must be ascii or p < n < a, where ispunct(p) &&
33
+ isdigit(n) && isalpha(c) (this handles UTF-8 collation).
34
+
35
+
36
+
37
+
38
+
39
+ Copyright (c) 2008 Antony Nigel Donovan, released under the MIT license
@@ -0,0 +1,351 @@
1
+ # ActsAsMaterializedPath
2
+ module ActiveRecord #:nodoc:
3
+ module Acts #:nodoc:
4
+ module MaterializedPath #:nodoc:
5
+ #TODO - What's are appropraite exception types to raise?
6
+ #Also, add more information to errors raised.
7
+ class InvalidDelimiter < RangeError
8
+ end
9
+ class InvalidBase < RangeError
10
+ end
11
+ class InvalidAssignment < ArgumentError
12
+ end
13
+ class PathMaxExceeded < StandardError
14
+ end
15
+ class PathUpdateDisallowed < StandardError
16
+ end
17
+ class DestroyNotLeaf < TypeError
18
+ end
19
+
20
+ def self.included(base)
21
+ base.extend(ClassMethods)
22
+ end
23
+
24
+ module ClassMethods
25
+
26
+ #
27
+ def acts_as_materialized_path(options = {})
28
+ conf = {
29
+ :delimiter => '.',
30
+ :base => 10,
31
+ :column => 'materialized_path',
32
+ :places => 3,
33
+ }
34
+ conf.update(options) if options.is_a?(Hash)
35
+
36
+ unless conf[:delimiter].length == 1 &&
37
+ (' '..'/') === conf[:delimiter]
38
+ raise InvalidDelimiter
39
+ end
40
+
41
+ unless (2..36) === conf[:base]
42
+ raise InvalidBase
43
+ end
44
+
45
+ conf[:adapter] =
46
+ ActiveRecord::Base.connection.adapter_name
47
+
48
+ #configuration settings
49
+ write_inheritable_attribute :mp_delimiter, conf[:delimiter]
50
+ class_inheritable_reader :mp_delimiter
51
+
52
+ write_inheritable_attribute :mp_base, conf[:base]
53
+ class_inheritable_reader :mp_base
54
+
55
+ write_inheritable_attribute :mp_column, conf[:column]
56
+ class_inheritable_reader :mp_column
57
+
58
+ write_inheritable_attribute :mp_places, conf[:places]
59
+ class_inheritable_reader :mp_places
60
+
61
+ #sql helpers
62
+ write_inheritable_attribute :mp_like, "#{mp_column} like ?"
63
+ class_inheritable_reader :mp_like
64
+
65
+ write_inheritable_attribute :mp_eq, "#{mp_column} = ?"
66
+ class_inheritable_reader :mp_eq
67
+
68
+ write_inheritable_attribute :mp_gt, "#{mp_column} > ?"
69
+ class_inheritable_reader :mp_gt
70
+
71
+ write_inheritable_attribute :mp_gte, "#{mp_column} >= ?"
72
+ class_inheritable_reader :mp_gte
73
+
74
+ write_inheritable_attribute :mp_lt, "#{mp_column} < ?"
75
+ class_inheritable_reader :mp_lt
76
+
77
+ write_inheritable_attribute :mp_asc, "#{mp_column} ASC"
78
+ class_inheritable_reader :mp_asc
79
+
80
+ write_inheritable_attribute :mp_desc, "#{mp_column} DESC"
81
+ class_inheritable_reader :mp_desc
82
+
83
+ limit_string = '~' # This only works with and asci collating sequence
84
+ limit_string = 'Z'*(mp_places+1) # This works with UTF-8
85
+
86
+ write_inheritable_attribute :mp_ancestor_limit,
87
+ case conf[:adapter]
88
+ when 'MySQL'
89
+ "concat(#{mp_column}, '#{limit_string}')"
90
+ when 'SQLServer'
91
+ "#{mp_column} + '#{limit_string}'"
92
+ else # ANSI SQL Syntax
93
+ "#{mp_column} || '#{limit_string}'"
94
+ end
95
+ class_inheritable_reader :mp_ancestor_limit
96
+
97
+ write_inheritable_attribute :mp_between,
98
+ "? between #{mp_column} and #{mp_ancestor_limit}"
99
+ class_inheritable_reader :mp_between
100
+
101
+ #path manipulation fu
102
+ write_inheritable_attribute :mp_regexp,
103
+ Regexp.new("[[:alnum:]]{#{conf[:places]}}\\#{conf[:delimiter]}$")
104
+ class_inheritable_reader :mp_regexp
105
+
106
+
107
+ include ActiveRecord::Acts::MaterializedPath::InstanceMethods
108
+ extend ActiveRecord::Acts::MaterializedPath::SingletonMethods
109
+
110
+ #before_create :before_create_callback
111
+
112
+ attr_protected conf[:column].to_sym
113
+
114
+
115
+ #if parent set, save as child
116
+ #if sibling set, save as sibling
117
+ #else save as root
118
+ attr_accessor :mp_parent_id_for_save
119
+ attr_accessor :mp_sibling_id_for_save
120
+
121
+ #this mucks up emacs indenting, so watch out for that
122
+ class_eval <<-EOV
123
+ def #{mp_column}=(newpath)
124
+ raise InvalidAssignment
125
+ end
126
+ EOV
127
+
128
+ end
129
+ end
130
+
131
+ module SingletonMethods
132
+ #
133
+ def roots
134
+ siblings('')
135
+ end
136
+
137
+ #
138
+ def num2path_string(num)#:nodoc:
139
+ str = num.to_s(mp_base)
140
+ len = str.length
141
+
142
+ raise PathMaxExceeded unless len <= mp_places
143
+
144
+ '0'*(mp_places-len)+str
145
+ end
146
+
147
+ #utility funtion to return a set of siblings
148
+ def siblings(path, select = '*')
149
+ find( :all,
150
+ :select => select,
151
+ :conditions =>
152
+ [ mp_like, path + '_' * mp_places + mp_delimiter],
153
+ :order => mp_asc )
154
+ end
155
+
156
+ def inner_delete(id)#:nodoc:
157
+ c = find(id, :select => mp_column)
158
+ rescue RecordNotFound then
159
+ return 0
160
+ else
161
+ count = 0
162
+ c.children.each do |child|
163
+ count += self.delete(child.id)
164
+ end
165
+ return count
166
+ end
167
+
168
+ #FIXME - handle arrays of ids
169
+ def delete(id)
170
+ transaction do
171
+ inner_delete(id) + super(id)
172
+ end
173
+ end
174
+ end
175
+
176
+ module InstanceMethods
177
+ #
178
+ def left_most_child
179
+ self.class.find(:first,
180
+ :conditions =>
181
+ ["#{mp_like}",
182
+ the_path + '_' * mp_places + mp_delimiter],
183
+ :order => mp_asc)
184
+ end
185
+
186
+ def is_leaf?
187
+ !left_most_child
188
+ end
189
+
190
+ #
191
+ def right_sibling
192
+ self.class.find(:first,
193
+ :conditions =>
194
+ ["#{mp_gt} and #{mp_lt} and length(#{mp_column}) = ?",
195
+ the_path,
196
+ the_path(true)+'~',
197
+ the_path.length],
198
+ :order => mp_asc)
199
+ end
200
+
201
+ #
202
+ def destroy
203
+ raise DestroyNotLeaf unless children.length == 0
204
+ #or
205
+ # self.class.transaction do
206
+ # children.each do |child|
207
+ # child.destroy
208
+ # end
209
+ # end
210
+ super
211
+ end
212
+
213
+ #
214
+ def set_path(path)
215
+ write_attribute(mp_column.to_sym, path)
216
+ end
217
+
218
+ #
219
+ def nextvalue(path)
220
+ last = self.class.find(:first,
221
+ :select => mp_column,
222
+ :conditions =>
223
+ [mp_like, path +
224
+ '_' * mp_places +
225
+ mp_delimiter],
226
+ :order => mp_desc,
227
+ :lock => true)
228
+
229
+ if last
230
+ last_path = last.the_path
231
+ if i = last_path.index(mp_regexp)
232
+ leaf = last_path[i, mp_places]
233
+ else
234
+ leaf = last_path
235
+ end
236
+ nextval = leaf.to_i(mp_base).next
237
+ else
238
+ nextval = 0
239
+ end
240
+
241
+ return nextval
242
+ end
243
+
244
+ #
245
+ def save
246
+ if new_record? && !the_path
247
+ self.class.transaction do
248
+ basepath = ''
249
+ if mp_parent_id_for_save.to_i > 0
250
+ relation = mp_parent_id_for_save
251
+ sibling = false
252
+ elsif mp_sibling_id_for_save.to_i > 0
253
+ relation = mp_sibling_id_for_save
254
+ sibling = true
255
+ end
256
+ basepath =
257
+ self.class.find(relation).the_path(sibling) if relation
258
+
259
+ nextval = nextvalue(basepath)
260
+ set_path(basepath+self.class.num2path_string(nextval)+mp_delimiter)
261
+ super
262
+ end
263
+ else
264
+ super
265
+ end
266
+ end
267
+
268
+ #
269
+
270
+ #
271
+ def save_as_relation(parent_or_sibling, sibling)
272
+ raise PathUpdateDisallowed unless new_record? && !the_path
273
+ self.class.transaction do
274
+ basepath = parent_or_sibling.the_path(sibling)
275
+ nextval = nextvalue(basepath)
276
+ set_path(basepath+self.class.num2path_string(nextval)+mp_delimiter)
277
+ save
278
+ end
279
+ end
280
+
281
+ #
282
+ def save_as_sibling_of(sibling)
283
+ save_as_relation(sibling, true)
284
+ end
285
+
286
+ #
287
+ def save_as_child_of(parent)
288
+ save_as_relation(parent, false)
289
+ end
290
+
291
+ #
292
+ def the_path(truncate = false)
293
+ materialized_path = self.send(mp_column)
294
+ if truncate
295
+ rex = Regexp.new('[[:alnum:]]{' + mp_places.to_s + '}' +
296
+ '\\' + mp_delimiter)
297
+ offset = materialized_path.rindex(rex)
298
+ end
299
+ materialized_path = materialized_path[0, offset] if offset
300
+ return materialized_path
301
+ end
302
+
303
+ #
304
+ def siblings(include_self = false)
305
+ res = self.class.siblings(the_path(true))
306
+ res.delete_if{|mp| mp.the_path == the_path} unless include_self
307
+ return res
308
+ end
309
+
310
+ # returns an array of children (empty if this is a leaf)
311
+ def children
312
+ self.class.siblings(the_path)
313
+ end
314
+
315
+ #
316
+ def parent
317
+ self.class.find(:first,
318
+ :conditions =>
319
+ [ mp_eq,
320
+ the_path(true) ])
321
+ end
322
+
323
+ # returns an array of parents to root
324
+ def ancestors(include_self = false)
325
+ self.class.find(:all,
326
+ :conditions =>
327
+ [ mp_between,
328
+ the_path(!include_self) ],
329
+ :order => mp_asc )
330
+ end
331
+
332
+ #returns the depth of this node in the tree (0 based)
333
+ def depth
334
+ the_path.count(mp_delimiter)-1
335
+ end
336
+
337
+ #TODO
338
+ def descendants(include_self = false)
339
+ self.class.find(:all,
340
+ :conditions =>
341
+ [ mp_like,
342
+ "#{the_path}%" ],
343
+ :order => mp_asc )
344
+ end
345
+
346
+ end
347
+ end
348
+ end
349
+ end
350
+
351
+ ActiveRecord::Base.send(:include, ActiveRecord::Acts::MaterializedPath)
metadata ADDED
@@ -0,0 +1,63 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: oakridgelanl-materialized-path
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.0.2
5
+ platform: ruby
6
+ authors:
7
+ - Antony Donovan
8
+ autorequire:
9
+ bindir: bin
10
+ cert_chain: []
11
+
12
+ date: 2008-05-15 00:00:00 -07:00
13
+ default_executable:
14
+ dependencies:
15
+ - !ruby/object:Gem::Dependency
16
+ name: activerecord
17
+ version_requirement:
18
+ version_requirements: !ruby/object:Gem::Requirement
19
+ requirements:
20
+ - - ">="
21
+ - !ruby/object:Gem::Version
22
+ version: 2.0.2
23
+ version:
24
+ description: An ActsAs mixin implementing a set of trees (nested set) for ActiveRecord
25
+ email: and@prospectmarkets.com
26
+ executables: []
27
+
28
+ extensions: []
29
+
30
+ extra_rdoc_files:
31
+ - README
32
+ files:
33
+ - README
34
+ - lib/acts_as_materialized_path.rb
35
+ has_rdoc: true
36
+ homepage:
37
+ post_install_message:
38
+ rdoc_options:
39
+ - --main
40
+ - README
41
+ require_paths:
42
+ - lib
43
+ required_ruby_version: !ruby/object:Gem::Requirement
44
+ requirements:
45
+ - - ">="
46
+ - !ruby/object:Gem::Version
47
+ version: "0"
48
+ version:
49
+ required_rubygems_version: !ruby/object:Gem::Requirement
50
+ requirements:
51
+ - - ">="
52
+ - !ruby/object:Gem::Version
53
+ version: "0"
54
+ version:
55
+ requirements: []
56
+
57
+ rubyforge_project:
58
+ rubygems_version: 1.0.1
59
+ signing_key:
60
+ specification_version: 2
61
+ summary: An ActsAs mixin implementing a set of trees (nested set) for ActiveRecord
62
+ test_files: []
63
+