strokedb 0.0.2

Sign up to get free protection for your applications and to get access to all the features.
Files changed (59) hide show
  1. data/CONTRIBUTORS +7 -0
  2. data/CREDITS +13 -0
  3. data/README +44 -0
  4. data/bin/sdbc +2 -0
  5. data/lib/config/config.rb +161 -0
  6. data/lib/data_structures/inverted_list.rb +297 -0
  7. data/lib/data_structures/point_query.rb +24 -0
  8. data/lib/data_structures/skiplist.rb +302 -0
  9. data/lib/document/associations.rb +107 -0
  10. data/lib/document/callback.rb +11 -0
  11. data/lib/document/coercions.rb +57 -0
  12. data/lib/document/delete.rb +28 -0
  13. data/lib/document/document.rb +684 -0
  14. data/lib/document/meta.rb +261 -0
  15. data/lib/document/slot.rb +199 -0
  16. data/lib/document/util.rb +27 -0
  17. data/lib/document/validations.rb +704 -0
  18. data/lib/document/versions.rb +106 -0
  19. data/lib/document/virtualize.rb +82 -0
  20. data/lib/init.rb +57 -0
  21. data/lib/stores/chainable_storage.rb +57 -0
  22. data/lib/stores/inverted_list_index/inverted_list_file_storage.rb +56 -0
  23. data/lib/stores/inverted_list_index/inverted_list_index.rb +49 -0
  24. data/lib/stores/remote_store.rb +172 -0
  25. data/lib/stores/skiplist_store/chunk.rb +119 -0
  26. data/lib/stores/skiplist_store/chunk_storage.rb +21 -0
  27. data/lib/stores/skiplist_store/file_chunk_storage.rb +44 -0
  28. data/lib/stores/skiplist_store/memory_chunk_storage.rb +37 -0
  29. data/lib/stores/skiplist_store/skiplist_store.rb +217 -0
  30. data/lib/stores/store.rb +5 -0
  31. data/lib/sync/chain_sync.rb +38 -0
  32. data/lib/sync/diff.rb +126 -0
  33. data/lib/sync/lamport_timestamp.rb +81 -0
  34. data/lib/sync/store_sync.rb +79 -0
  35. data/lib/sync/stroke_diff/array.rb +102 -0
  36. data/lib/sync/stroke_diff/default.rb +21 -0
  37. data/lib/sync/stroke_diff/hash.rb +186 -0
  38. data/lib/sync/stroke_diff/string.rb +116 -0
  39. data/lib/sync/stroke_diff/stroke_diff.rb +9 -0
  40. data/lib/util/blankslate.rb +42 -0
  41. data/lib/util/ext/blank.rb +50 -0
  42. data/lib/util/ext/enumerable.rb +36 -0
  43. data/lib/util/ext/fixnum.rb +16 -0
  44. data/lib/util/ext/hash.rb +22 -0
  45. data/lib/util/ext/object.rb +8 -0
  46. data/lib/util/ext/string.rb +35 -0
  47. data/lib/util/inflect.rb +217 -0
  48. data/lib/util/java_util.rb +9 -0
  49. data/lib/util/lazy_array.rb +54 -0
  50. data/lib/util/lazy_mapping_array.rb +64 -0
  51. data/lib/util/lazy_mapping_hash.rb +46 -0
  52. data/lib/util/serialization.rb +29 -0
  53. data/lib/util/trigger_partition.rb +136 -0
  54. data/lib/util/util.rb +38 -0
  55. data/lib/util/xml.rb +6 -0
  56. data/lib/view/view.rb +55 -0
  57. data/script/console +70 -0
  58. data/strokedb.rb +75 -0
  59. metadata +148 -0
@@ -0,0 +1,102 @@
1
+ module StrokeDB
2
+ class ::Array
3
+ SDATPTAGS = {
4
+ '-' => PATCH_MINUS,
5
+ '+' => PATCH_PLUS,
6
+ '!' => PATCH_DIFF
7
+ }.freeze
8
+ def stroke_diff(to)
9
+ return super(to) unless Array === to
10
+ return nil if self == to
11
+
12
+ # sdiff: + - ! =
13
+ lcs_sdiff = ::Diff::LCS.sdiff(self, to)
14
+ patchset = lcs_sdiff.inject([]) do |patchset, change|
15
+ a = SDATPTAGS[change.action]
16
+ if a == PATCH_DIFF
17
+ patchset << [a, change.new_position, change.old_element.stroke_diff(change.new_element)]
18
+ elsif a == PATCH_MINUS
19
+ patchset << [a, change.new_position]
20
+ elsif a == PATCH_PLUS
21
+ patchset << [a, change.new_position, change.new_element]
22
+ end
23
+ patchset
24
+ end
25
+ patchset.empty? ? nil : patchset
26
+ end
27
+
28
+ def stroke_patch(patch)
29
+ return self unless patch
30
+ return patch[1] if patch[0] == PATCH_REPLACE
31
+ res = self.dup
32
+ patch.each do |change|
33
+ _stroke_elementary_patch(res, change[1], change)
34
+ end
35
+ res
36
+ end
37
+
38
+ def stroke_merge(patch1, patch2)
39
+ unless patch1 && patch2
40
+ return _stroke_automerged(stroke_patch(patch1 || patch2))
41
+ end
42
+
43
+ patch1 = patch1.dup
44
+ patch2 = patch2.dup
45
+
46
+ c1 = patch1.shift
47
+ c2 = patch2.shift
48
+
49
+ offset1 = 0
50
+ offset2 = 0
51
+ result = self.dup
52
+ result1 = nil
53
+ result2 = nil
54
+
55
+ while c1 && c2
56
+ while c1 && (p1 = c1[1] + offset1) && (p2 = c2[1] + offset2) && p1 < p2
57
+ offset2 += _stroke_elementary_patch(result, p1, c1)
58
+ c1 = patch1.shift
59
+ end
60
+
61
+ if p1 == p2
62
+ raise "TODO conflict resolution!"
63
+
64
+ c1 = patch1.shift
65
+ end
66
+
67
+ while c1 && c2 && (p1 = c1[1] + offset1) && (p2 = c2[1] + offset2) && p2 < p1
68
+ offset1 += _stroke_elementary_patch(result, p2, c2)
69
+ c2 = patch2.shift
70
+ end
71
+ end
72
+
73
+ # Tail (one of two)
74
+ while c1
75
+ offset2 += _stroke_elementary_patch(result, c1[1] + offset1, c1)
76
+ c1 = patch1.shift
77
+ end
78
+ while c2
79
+ offset1 += _stroke_elementary_patch(result, c2[1] + offset2, c2)
80
+ c2 = patch2.shift
81
+ end
82
+ result ? _stroke_automerged(result) : _stroke_conflicted(result1, result2)
83
+ end
84
+
85
+ private
86
+ def _stroke_elementary_patch(result, pos, change)
87
+ a = change[0]
88
+ case a
89
+ when PATCH_MINUS
90
+ result.delete_at(pos)
91
+ -1
92
+ when PATCH_PLUS
93
+ result[pos, 0] = [change[2]]
94
+ +1
95
+ when PATCH_DIFF
96
+ result[pos] = result[pos].stroke_patch(change[2])
97
+ 0
98
+ end
99
+ end
100
+
101
+ end
102
+ end
@@ -0,0 +1,21 @@
1
+ module StrokeDB
2
+ class ::Object
3
+ def stroke_diff(to)
4
+ self == to ? nil : [PATCH_REPLACE, to]
5
+ end
6
+ def stroke_patch(patch)
7
+ patch ? patch[1] : self
8
+ end
9
+ def stroke_merge(patch1, patch2) # => is_conflict, result1, result2
10
+ r1 = self.stroke_patch(patch1)
11
+ r2 = self.stroke_patch(patch2)
12
+ [r1 != r2, r1, r2]
13
+ end
14
+ def _stroke_automerged(r)
15
+ [false, r, r]
16
+ end
17
+ def _stroke_conflicted(r1, r2)
18
+ [true, r1, r2]
19
+ end
20
+ end
21
+ end
@@ -0,0 +1,186 @@
1
+ module StrokeDB
2
+ class ::Hash
3
+ def stroke_diff(to)
4
+ return super(to) unless Hash === to
5
+ return nil if self == to
6
+
7
+ all_keys = self.keys | to.keys
8
+
9
+ deleted_slots = []
10
+ inserted_slots = {}
11
+ diffed_slots = {}
12
+
13
+ all_keys.each do |k|
14
+ unless to.key?(k)
15
+ deleted_slots << k
16
+ else
17
+ unless self.key?(k)
18
+ inserted_slots[k] = to[k]
19
+ else
20
+ diff = self[k].stroke_diff(to[k])
21
+ diffed_slots[k] = diff if diff
22
+ end
23
+ end
24
+ end
25
+ [deleted_slots, inserted_slots, diffed_slots]
26
+ end
27
+
28
+ def stroke_patch(patch)
29
+ return self unless patch
30
+ return patch[1] if patch[0] == PATCH_REPLACE
31
+ res = self.dup
32
+ deleted_slots, inserted_slots, diffed_slots = patch
33
+ deleted_slots.each {|k| res.delete(k) }
34
+ res.merge!(inserted_slots)
35
+ diffed_slots.each {|k,v| res[k] = self[k].stroke_patch(v) }
36
+ res
37
+ end
38
+
39
+ # Hash conflict may occur:
40
+ # 1) If key accures in conflicting groups
41
+ # (e.g. deleted v.s. inserted, deleted vs. diffed)
42
+ # 2) If slot diff yields conflict
43
+ #
44
+ def stroke_merge(patch1, patch2)
45
+ unless patch1 && patch2
46
+ return _stroke_automerged(stroke_patch(patch1 || patch2))
47
+ end
48
+
49
+ del1, ins1, dif1 = patch1[0].dup, patch1[1].dup, patch1[2].dup
50
+ del2, ins2, dif2 = patch2[0].dup, patch2[1].dup, patch2[2].dup
51
+
52
+ # TEMPORARY: inconsistency check
53
+ conflict_d1i2 = del1 & ins2.keys
54
+ conflict_d2i1 = del2 & ins1.keys
55
+ conflict_i1f2 = ins1.keys & dif2.keys
56
+ conflict_i2f1 = ins2.keys & dif1.keys
57
+
58
+ unless conflict_d1i2.empty? && conflict_d2i1.empty? &&
59
+ conflict_i1f2.empty? && conflict_i2f1.empty?
60
+ raise "Fatal inconsistency on stroke_merge detected!"
61
+ end
62
+
63
+ overlapping_keys = ((del1 + ins1.keys + dif1.keys) &
64
+ (del2 + ins2.keys + dif2.keys))
65
+ # make hash for faster inclusion tests
66
+ overlapping_keys_hash = overlapping_keys.inject({}) do |h, k|
67
+ h[k] = 1; h
68
+ end
69
+
70
+ result = self.dup
71
+
72
+ # 1. Merge non-overlapping updates
73
+ (del1 + del2 - overlapping_keys).each do |k|
74
+ del1.delete(k)
75
+ del2.delete(k)
76
+ result.delete(k)
77
+ end
78
+ ins1.dup.each do |k, v|
79
+ unless overlapping_keys_hash[k]
80
+ result[k] = v
81
+ ins1.delete(k)
82
+ end
83
+ end
84
+ ins2.dup.each do |k, v|
85
+ unless overlapping_keys_hash[k]
86
+ result[k] = v
87
+ ins2.delete(k)
88
+ end
89
+ end
90
+ dif1.dup.each do |k, diff|
91
+ unless overlapping_keys_hash[k]
92
+ result[k] = stroke_patch(diff)
93
+ dif1.delete(k)
94
+ end
95
+ end
96
+ dif2.dup.each do |k, diff|
97
+ unless overlapping_keys_hash[k]
98
+ result[k] = stroke_patch(diff)
99
+ dif2.delete(k)
100
+ end
101
+ end
102
+
103
+ # 2. Resolve overlapping keys
104
+ #
105
+ # Overlapping key may be in such pairs:
106
+ #
107
+ # [dif, dif] <- possible conflict
108
+ # [dif, ins] <- anomaly
109
+ # [ins, ins] <- possible conflict
110
+ # [del, ins] <- anomaly
111
+ # [del, dif] <- conflict
112
+ # [del, del] <- not a conflict
113
+ #
114
+ # (and in reverse order as well)
115
+
116
+ result1 = nil
117
+ result2 = nil
118
+ del1.each do |k|
119
+ if ins2.key?(k)
120
+ raise "Fatal inconsistency on stroke_merge detected: delete + insert"
121
+ elsif dif2.key?(k)
122
+ # Conflict. Split result if not splitted.
123
+ result1, result2, result = _stroke_split_merge_result(result) if result
124
+ result1.delete(k)
125
+ result2[k] = result2[k].stroke_patch(dif2[k])
126
+ else # [del, del]
127
+ if result
128
+ result.delete(k)
129
+ else
130
+ result1.delete(k)
131
+ result2.delete(k)
132
+ end
133
+ end
134
+ end
135
+ dif1.each do |k, diff|
136
+ if ins2.key?(k)
137
+ raise "Fatal inconsistency on stroke_merge detected: diff + insert"
138
+ elsif dif2.key?(k) # possible conflict
139
+ conflict, r1, r2 = self[k].stroke_merge(diff, dif2[k])
140
+ if conflict
141
+ result1, result2, result = _stroke_split_merge_result(result) if result
142
+ result1[k] = r1
143
+ result2[k] = r2
144
+ else
145
+ if result
146
+ result[k] = r2
147
+ else
148
+ result1[k] = r2
149
+ result2[k] = r2
150
+ end
151
+ end
152
+ else # [dif, del] <- conflict
153
+ # Conflict. Split result if not splitted.
154
+ result1, result2, result = _stroke_split_merge_result(result) if result
155
+ result1[k] = result1[k].stroke_patch(diff)
156
+ result2.delete(k)
157
+ end
158
+ end
159
+ ins1.each do |k, obj|
160
+ if ins2.key?(k) # possible conflict
161
+ if obj != ins2[k]
162
+ result1, result2, result = _stroke_split_merge_result(result) if result
163
+ result1[k] = obj
164
+ result2[k] = ins2[k]
165
+ else
166
+ if result
167
+ result[k] = obj
168
+ else
169
+ result1[k] = obj
170
+ result2[k] = obj
171
+ end
172
+ end
173
+ else # delete or diff
174
+ raise "Fatal inconsistency on stroke_merge detected: insert + (delete|diff)"
175
+ end
176
+ end
177
+
178
+ result ? _stroke_automerged(result) : _stroke_conflicted(result1, result2)
179
+ end
180
+
181
+ # In case of conflict, result is copied to result{1,2} and nullified.
182
+ def _stroke_split_merge_result(result)
183
+ return [result.dup, result.dup, nil]
184
+ end
185
+ end
186
+ end
@@ -0,0 +1,116 @@
1
+ module StrokeDB
2
+ class ::String
3
+ def stroke_diff(to)
4
+ return super(to) unless String === to
5
+ return nil if self == to
6
+
7
+ _f = self[0,2]
8
+ _t = to[0,2]
9
+ pfx = "@#"
10
+ # both are refs
11
+ return super(to) if _f == _t && _t == pfx
12
+ # one of items is ref, another is not.
13
+ return super(to) if _f == pfx || _t == pfx
14
+
15
+ lcs_diff = ::Diff::LCS.diff(self, to)
16
+ patchset = lcs_diff.map do |changes|
17
+ parts = []
18
+ last_part = changes.inject(nil) do |part, change|
19
+ if part && part[0] == change.action && part[3] == change.position - 1
20
+ part[3] += 1
21
+ part[2] << change.element
22
+ part
23
+ else
24
+ parts << part if part
25
+ # emit
26
+ [change.action, change.position, change.element, change.position]
27
+ end
28
+ end
29
+ parts << last_part if last_part
30
+ parts.empty? ? nil : parts
31
+ end.compact.inject([]) do |patches, ps|
32
+ ps.map do |p|
33
+ patches << if p[0] == '+'
34
+ [PATCH_PLUS, p[1], p[2]] # [+ position_in_b substr]
35
+ else
36
+ [PATCH_MINUS, p[1], p[2].size] # [- position_in_a length]
37
+ end
38
+ end
39
+ patches
40
+ end
41
+ #p patchset
42
+ patchset.empty? ? nil : patchset
43
+ end
44
+
45
+ def stroke_patch(patch)
46
+ return self unless patch
47
+ return patch[1] if patch[0] == PATCH_REPLACE
48
+
49
+ # Patch is a list of insertions and deletions.
50
+ # Deletion is indexed relative to base.
51
+ # Insertion is indexed relative to new string.
52
+ res = ""
53
+ ai = bj = 0
54
+ patch.each do |change|
55
+ action, position, element = change
56
+ case action
57
+ when PATCH_MINUS
58
+ d = position - ai
59
+ if d > 0
60
+ res << self[ai, d]
61
+ ai += d
62
+ bj += d
63
+ end
64
+ ai += element # element == length
65
+ when PATCH_PLUS
66
+ d = position - bj
67
+ if d > 0
68
+ res << self[ai, d]
69
+ ai += d
70
+ bj += d
71
+ end
72
+ bj += element.size
73
+ res << element
74
+ end
75
+ end
76
+ d = self.size - ai
77
+ res << self[ai, d] if d > 0
78
+ res
79
+ end
80
+
81
+ def stroke_merge(patch1, patch2)
82
+ # One patch is missing (i.e. no changes)
83
+ unless patch1 && patch2
84
+ return _stroke_automerged(stroke_patch(patch1 || patch2))
85
+ end
86
+
87
+ # Patch could be either PATCH_REPLACE or regular string diff.
88
+ # Thus, 4 cases:
89
+ #
90
+ # [replace, replace] -> possible conflict
91
+ # [replace, diff] -> conflict
92
+ # [diff, replace] -> conflict
93
+ # [diff, diff] -> possible conflict
94
+
95
+ # Code is verbose to be fast and clear
96
+ if patch1[0] == PATCH_REPLACE
97
+ if patch2[0] == PATCH_REPLACE # [replace, replace]
98
+ if patch1[1] != patch2[1]
99
+ return _stroke_conflicted(stroke_patch(patch1), stroke_patch(patch2))
100
+ else
101
+ return _stroke_automerged(stroke_patch(patch1))
102
+ end
103
+ else # [replace, diff]
104
+ return _stroke_conflicted(stroke_patch(patch1), stroke_patch(patch2))
105
+ end
106
+ else
107
+ if patch1[0] == PATCH_REPLACE # [diff, replace]
108
+ return _stroke_conflicted(stroke_patch(patch1), stroke_patch(patch2))
109
+ else
110
+ nil # [diff, diff] - see below
111
+ end
112
+ end
113
+ # TODO: ...
114
+ end
115
+ end
116
+ end
@@ -0,0 +1,9 @@
1
+ require 'rubygems'
2
+ require 'diff/lcs'
3
+
4
+ module StrokeDB
5
+ PATCH_REPLACE = 'R'.freeze
6
+ PATCH_PLUS = '+'.freeze
7
+ PATCH_MINUS = '-'.freeze
8
+ PATCH_DIFF = 'D'.freeze
9
+ end
@@ -0,0 +1,42 @@
1
+ unless defined?(BlankSlate)
2
+ class BlankSlate < BasicObject; end if defined?(BasicObject)
3
+
4
+ class BlankSlate
5
+ instance_methods.each { |m| undef_method m unless m =~ /^__/ }
6
+ end
7
+ end
8
+
9
+ class BlankSlate
10
+ MethodMapping = {
11
+ '[]' => 'squarebracket',
12
+ '[]=' => 'squarebracket_set',
13
+ '<<' => 'leftarrow',
14
+ '*' => 'star',
15
+ '+' => 'plus',
16
+ '-' => 'minus',
17
+ '&' => 'bitwiseand',
18
+ '|' => 'bitwiseor',
19
+ '<=>' => 'spaceship',
20
+ '==' => 'equalequal',
21
+ '===' => 'tripleequal',
22
+ '=~' => 'regexmatch',
23
+ '`' => 'backtick',
24
+ } unless defined? MethodMapping
25
+ end
26
+
27
+ def BlankSlate superclass = nil
28
+ if superclass
29
+ (@blank_slates ||= {})[superclass] ||= Class.new(superclass) do
30
+ instance_methods.sort.each { |m|
31
+ unless m =~ /^__/
32
+ mname = "__#{::BlankSlate::MethodMapping[m.to_s] || m}"
33
+ class_eval "alias :'#{mname}' :'#{m}'"
34
+
35
+ undef_method m
36
+ end
37
+ }
38
+ end
39
+ else
40
+ BlankSlate
41
+ end
42
+ end
@@ -0,0 +1,50 @@
1
+ class Object
2
+ # An object is blank if it's nil, empty, or a whitespace string.
3
+ # For example, "", " ", nil, [], and {} are blank.
4
+ #
5
+ # This simplifies
6
+ # if !address.nil? && !address.empty?
7
+ # to
8
+ # if !address.blank?
9
+ def blank?
10
+ respond_to?(:empty?) ? empty? : !self
11
+ end
12
+ end
13
+
14
+ class NilClass #:nodoc:
15
+ def blank?
16
+ true
17
+ end
18
+ end
19
+
20
+ class FalseClass #:nodoc:
21
+ def blank?
22
+ true
23
+ end
24
+ end
25
+
26
+ class TrueClass #:nodoc:
27
+ def blank?
28
+ false
29
+ end
30
+ end
31
+
32
+ class Array #:nodoc:
33
+ alias_method :blank?, :empty?
34
+ end
35
+
36
+ class Hash #:nodoc:
37
+ alias_method :blank?, :empty?
38
+ end
39
+
40
+ class String #:nodoc:
41
+ def blank?
42
+ self !~ /\S/
43
+ end
44
+ end
45
+
46
+ class Numeric #:nodoc:
47
+ def blank?
48
+ false
49
+ end
50
+ end
@@ -0,0 +1,36 @@
1
+ # extracted from ActiveRecord (http://rubyforge.org/projects/activesupport/)
2
+
3
+ module Enumerable
4
+ # Collect an enumerable into sets, grouped by the result of a block. Useful,
5
+ # for example, for grouping records by date.
6
+ def group_by
7
+ inject({}) do |groups, element|
8
+ (groups[yield(element)] ||= []) << element
9
+ groups
10
+ end
11
+ end if RUBY_VERSION < '1.9'
12
+
13
+ # Map and each_with_index combined.
14
+ def map_with_index
15
+ collected=[]
16
+ each_with_index {|item, index| collected << yield(item, index) }
17
+ collected
18
+ end
19
+
20
+ alias :collect_with_index :map_with_index
21
+
22
+ def each_consecutive_pair
23
+ first = true
24
+ prev = nil
25
+
26
+ each do |val|
27
+ unless first
28
+ yield prev, val
29
+ else
30
+ first = false
31
+ end
32
+
33
+ prev = val
34
+ end
35
+ end
36
+ end
@@ -0,0 +1,16 @@
1
+ # extracted and adapted from ActiveSupport (http://rubyforge.org/projects/activesupport/)
2
+
3
+ class Fixnum
4
+ def multiple_of?(number)
5
+ self % number == 0
6
+ end
7
+
8
+ def even?
9
+ multiple_of? 2
10
+ end
11
+
12
+ def odd?
13
+ !even?
14
+ end
15
+ end
16
+
@@ -0,0 +1,22 @@
1
+ # extracted and adapted from ActiveRecord (http://rubyforge.org/projects/activesupport/)
2
+
3
+ class Hash
4
+ def stringify_keys
5
+ inject({}) do |options, (key, value)|
6
+ options[key.to_s] = value
7
+ options
8
+ end
9
+ end
10
+
11
+ def except(*keys)
12
+ reject { |key,| keys.include?(key.to_sym) or keys.include?(key.to_s) }
13
+ end
14
+
15
+ def reverse_merge(other_hash)
16
+ other_hash.merge(self)
17
+ end
18
+
19
+ def reverse_merge!(other_hash)
20
+ replace(reverse_merge(other_hash))
21
+ end
22
+ end
@@ -0,0 +1,8 @@
1
+ # extracted from ActiveRecord (http://rubyforge.org/projects/activesupport/)
2
+
3
+ class Object
4
+ unless respond_to?(:send!)
5
+ # Anticipating Ruby 1.9 neutering send
6
+ alias send! send
7
+ end
8
+ end
@@ -0,0 +1,35 @@
1
+ # extracted and adapted from ActiveRecord (http://rubyforge.org/projects/activesupport/)
2
+
3
+ class String
4
+ def ends_with?(suffix)
5
+ suffix = suffix.to_s
6
+ self[-suffix.length, suffix.length] == suffix
7
+ end
8
+
9
+ def camel_case
10
+ split('_').map{|e| e.capitalize}.join
11
+ end
12
+ alias :camelize :camel_case
13
+
14
+ def snake_case
15
+ gsub(/\B[A-Z][^A-Z]/, '_\&').downcase.gsub(' ', '_')
16
+ end
17
+
18
+ def tableize
19
+ words = snake_case.split('_')
20
+ words.last.replace words.last.plural
21
+ words.join('_')
22
+ end
23
+
24
+ def demodulize
25
+ gsub(/^.*::/, '')
26
+ end
27
+
28
+ def constantize
29
+ unless /\A(?:::)?([A-Z]\w*(?:::[A-Z]\w*)*)\z/ =~ self
30
+ raise NameError, "#{self.inspect} is not a valid constant name!"
31
+ end
32
+
33
+ Object.module_eval("::#{$1}", __FILE__, __LINE__)
34
+ end
35
+ end